49% of cyberattacks in 2024 targeted APIs — up from 23% in 2023. The attack surface doubled in two years because API adoption doubled and because API-specific vulnerabilities are still widely misunderstood. The defenses are not complicated, but they require systematic implementation: OAuth 2.0 for authorization, JWT for stateless identity propagation, rate limiting for abuse prevention, and CORS hardening for browser-based clients.
This guide covers API OAuth JWT authentication end to end: OAuth 2.0 grant types, JWT structure and signing algorithm selection, token rotation and revocation, rate limiting algorithm comparison (token bucket, sliding window, leaky bucket), CORS configuration hardening, and Zero Trust architecture principles. By the end, you will have a concrete implementation checklist for each layer.
Why API Security Requires Layered Defense
Modern APIs are not a single attack surface — they are dozens of surfaces, each requiring a different defense. A well-authenticated request can still be used to exfiltrate data through broken object-level authorization (accessing another user's records by substituting their ID). A well-authorized request can still be used to overwhelm the system through missing rate limits. Correct rate limiting can still fail against cross-origin script injection if CORS is misconfigured.
The cost of API breach is quantified: the average data breach from an API attack costs $4.8 million (Verizon DBIR). Financial sector APIs that go down cost $10 million per hour in lost transaction revenue. These are not edge cases — they are the baseline cost of inadequate API security architecture.
OAuth 2.0: The Authorization Standard for Production APIs
OAuth 2.0 is the industry standard for API authorization. It solves the delegated authorization problem: how does Application A access User B's resources on Service C without ever receiving User B's password?
The answer is a token exchange. The user authenticates directly with the authorization server. The authorization server issues a time-limited access token to the application. The application presents the token to the API. The API verifies the token without contacting the authorization server on every request.
OAuth 2.0 Grant Types
Authorization Code Flow with PKCE is the correct implementation for web applications, mobile apps, and single-page applications. PKCE (Proof Key for Code Exchange) adds a cryptographic challenge to the flow, preventing authorization code interception attacks:
// Generate PKCE challenge
function generatePKCE(): { verifier: string; challenge: string } {
const verifier = base64url(crypto.randomBytes(32));
const challenge = base64url(
crypto.createHash('sha256').update(verifier).digest()
);
return { verifier, challenge };
}
// Step 1: Redirect to authorization server
const { verifier, challenge } = generatePKCE();
sessionStorage.setItem('pkce_verifier', verifier);
const authUrl = new URL('https://auth.yourcompany.com/authorize');
authUrl.searchParams.set('client_id', 'your-client-id');
authUrl.searchParams.set('redirect_uri', 'https://app.yourcompany.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', generateStateToken());
window.location.href = authUrl.toString();
// Step 2: Exchange authorization code for tokens (backend)
const tokenResponse = await fetch('https://auth.yourcompany.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: 'https://app.yourcompany.com/callback',
client_id: 'your-client-id',
code_verifier: sessionStorage.getItem('pkce_verifier'),
}),
});
Client Credentials Flow is for machine-to-machine authorization — microservice-to-microservice communication, backend jobs, and server integrations where there is no user involved:
// Service A requests access to Service B
const tokenResponse = await fetch('https://auth.yourcompany.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'customer-service:read order-service:write',
}),
});
const { access_token, expires_in } = await tokenResponse.json();
// Cache the token until expires_in - 60 seconds
Token storage: access tokens on the backend, stored in memory or a short-TTL cache. Never in localStorage (XSS accessible). For browser applications with a backend, store in httpOnly cookies — inaccessible to JavaScript, protected against CSRF with SameSite=Strict or a CSRF token.
JWT: Structure, Signing, and Validation
JWT (JSON Web Token) is the token format most commonly used with OAuth 2.0. A JWT is a base64url-encoded JSON header, payload, and cryptographic signature:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9. // Header
eyJzdWIiOiJ1c3JfMTIzIiwiZW1haWwiOiJ1c2Vy // Payload
QGV4YW1wbGUuY29tIiwicm9sZXMiOlsiYWRtaW4i // (continued)
XSwiaWF0IjoxNzExOTI4MDAwLCJleHAiOjE3MTE5 // (continued)
MjkwMDB9. // Signature
RSASHA256_SIGNATURE
RS256 vs HS256 Signing
The signing algorithm choice has architectural consequences:
| Algorithm | Key type | Verification | Use case |
|---|---|---|---|
| HS256 | Shared secret | Any service with the secret | Single-service, simple deployments |
| RS256 | RSA private/public key pair | Any service with the public key | Microservices, distributed verification |
| ES256 | EC private/public key pair | Any service with the public key | Same as RS256, smaller key size |
RS256 is the correct choice for API architectures. Only the authorization server holds the private key. Every API service can verify tokens using the public key from the JWKS endpoint — no secret sharing required, no coordination needed when the key rotates.
import { expressjwt } from 'express-jwt';
import jwksRsa from 'jwks-rsa';
export const jwtValidation = expressjwt({
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 10,
jwksUri: 'https://auth.yourcompany.com/.well-known/jwks.json',
}),
audience: 'https://api.yourcompany.com',
issuer: 'https://auth.yourcompany.com/',
algorithms: ['RS256'],
});
JWT Security Practices
Token lifetime: access tokens should expire in 15 minutes or less. Short-lived tokens limit the damage window if a token is stolen. Refresh tokens (7-30 days) enable seamless re-authentication without user interaction.
Token rotation: every refresh token use should invalidate the previous refresh token and issue a new one. If a refresh token is stolen and used, the legitimate user's next refresh attempt will fail (the old token is already revoked), providing a detection signal.
Revocation: JWTs are stateless by design, which means revocation requires a blocklist. Redis is the standard implementation — store revoked token JTIs with an expiration matching the token's remaining lifetime:
// Revoke on logout
async function revokeToken(token: string): Promise<void> {
const decoded = jwt.decode(token) as JWTPayload;
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.setex(`revoked:${decoded.jti}`, ttl, '1');
}
}
// Check during validation
async function isTokenRevoked(jti: string): Promise<boolean> {
return (await redis.exists(`revoked:${jti}`)) === 1;
}
API OAuth JWT Authentication in Zero Trust Architecture
Zero Trust architecture applies to API systems the same principle it applies to network architecture: never assume a request is trusted because of its origin. A request from inside the data center is not implicitly trusted. A request from a previously authenticated session is not implicitly trusted at the next request.
Zero Trust Principles for API Design
Verify explicitly: every request authenticates, regardless of source. Internal microservices authenticate to each other using Client Credentials tokens. Mutual TLS provides transport-layer identity verification in addition to application-layer token verification.
Least privilege: tokens carry only the scopes required for the specific operation. A token scoped to orders:read cannot modify orders. The authorization server enforces scope constraints; API services enforce them again at the resource level:
// Scope enforcement
function requireScope(...requiredScopes: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const tokenScopes = (req.auth?.scope || '').split(' ');
const hasAllScopes = requiredScopes.every(s => tokenScopes.includes(s));
if (!hasAllScopes) {
return res.status(403).json({
error: 'insufficient_scope',
required: requiredScopes,
});
}
next();
};
}
router.post('/orders', jwtValidation, requireScope('orders:write'), createOrder);
router.get('/orders', jwtValidation, requireScope('orders:read'), listOrders);
Assume breach: design for the scenario where an attacker has obtained a valid token. Object-level authorization checks prevent that token from accessing other users' resources. Scope constraints limit what operations are possible. Audit logging captures every action for incident response.
Rate Limiting Algorithms
Rate limiting is the primary defense against API abuse: DDoS amplification, credential stuffing, scraping, and accidental overload from buggy clients. Algorithm selection affects both security effectiveness and legitimate user experience.
Token Bucket Algorithm
The token bucket maintains a capacity of tokens that refill at a fixed rate. Each request consumes one token. When the bucket is empty, requests are rejected until tokens refill.
Bucket capacity: 100 tokens
Refill rate: 10 tokens/second
Result: burst of 100 requests allowed, sustained rate of 10 req/s
Token bucket handles burst traffic well — a user can make 100 requests instantly (emptying the bucket) then continue at 10 requests per second as the bucket refills. This is the right algorithm for interactive APIs where users have legitimate burst patterns (loading a dashboard that triggers multiple parallel requests).
Sliding Window Algorithm
The sliding window counts requests in the last N seconds, where the window moves with time rather than resetting at a fixed interval.
Window: last 60 seconds
Limit: 60 requests
At 10:00:30, user has made 55 requests since 9:59:30
5 more requests allowed immediately
Sliding window is more precise than fixed window (which allows 2x the limit at window boundaries) and prevents the burst-at-boundary abuse pattern. More Redis memory and CPU than token bucket, but correct behavior for per-user rate limits.
Leaky Bucket Algorithm
The leaky bucket processes requests at a fixed rate, queuing excess requests. It does not reject requests immediately — it smooths out traffic to a constant rate.
Processing rate: 10 requests/second
Queue capacity: 100
Effect: any burst is smoothed to 10 req/s; excess is queued up to 100
Leaky bucket is the right algorithm for rate-limiting outgoing requests to third-party APIs — it guarantees you never exceed their rate limits. For incoming request limiting, token bucket is typically preferable because it gives users burst capacity.
Rate Limit Response
The 429 response should include enough information for clients to implement correct retry logic:
res.status(429).json({
error: 'Too Many Requests',
message: 'Rate limit exceeded',
retryAfter: Math.ceil(resetMs / 1000),
});
res.set({
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': Math.ceil(resetTime / 1000).toString(),
'Retry-After': Math.ceil(resetMs / 1000).toString(),
});
CORS Security Configuration
CORS (Cross-Origin Resource Sharing) controls which browser origins can make requests to your API. The browser enforces CORS; non-browser clients (curl, server-side code) are not subject to it. This distinction matters: CORS is not a server-side security control — it is a browser enforcement mechanism.
Insecure CORS Configuration
// DO NOT DO THIS — allows any origin
app.use(cors({ origin: '*', credentials: true }));
Wildcard CORS with credentials allowed is the most dangerous misconfiguration. Any website can make credentialed requests to your API from a user's browser — the browser treats your API as a trusted cross-origin resource.
Secure CORS Configuration
const allowedOrigins = new Set([
'https://app.yourcompany.com',
'https://admin.yourcompany.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null,
].filter(Boolean));
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (curl, Postman, server-to-server)
if (!origin || allowedOrigins.has(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS: origin ${origin} not allowed`));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
maxAge: 86400, // Cache preflight for 24 hours
}));
CORS violations are logged and alerted. A sudden spike in CORS rejections from an unexpected domain is an indicator of attempted cross-origin script injection.
API Security Checklist
Before any API endpoint goes to production, verify each layer:
Authentication and Authorization
- OAuth 2.0 or API key authentication implemented
- JWT signed with RS256 (asymmetric, not shared secret)
- Token lifetime: access tokens ≤15 minutes, refresh tokens 7-30 days
- Refresh token rotation on every use
- Token revocation via Redis JTI blocklist
- Object-level authorization:
req.auth.sub === resource.ownerId - Scope enforcement per endpoint
Rate Limiting
- Rate limiting applied at API gateway and application layer
- Separate limits for unauthenticated and authenticated requests
- Per-endpoint limits for expensive operations
- 429 response includes
Retry-Afterheader - DDoS protection at CDN/gateway layer
Data Security
- TLS 1.3 for all traffic (including internal services)
- mTLS for service-to-service communication
- Response filtering: return only authorized fields
- Input validation: reject malformed requests before business logic
- SQL/NoSQL injection prevention (parameterized queries)
CORS and Browser Security
- Explicit origin allowlist (no wildcards in production)
- Security headers:
Content-Security-Policy,Strict-Transport-Security,X-Frame-Options - CSRF token for state-changing operations from browser clients
Logging and Monitoring
- All authentication failures logged with IP, user agent, and timestamp
- Rate limit violations logged and alerted
- Anomaly detection for unusual usage patterns (burst from new IP, access at unusual hours)
- Audit trail for data access in regulated domains
Conclusion
API OAuth JWT authentication is not a single decision — it is a stack of decisions, each addressing a specific attack vector. OAuth 2.0 with PKCE handles delegated authorization without exposing credentials. JWT with RS256 provides stateless identity propagation that scales across distributed services. Token rotation limits the blast radius of token theft. Rate limiting algorithms (token bucket for interactive bursts, sliding window for per-user fairness) prevent abuse while accommodating legitimate traffic patterns. CORS hardening closes the cross-origin script injection surface.
Zero Trust unifies these layers with a single principle: no request is trusted by origin or prior state alone. Every request authenticates, scopes are enforced, and the system is designed for the scenario where one layer fails. That architectural posture is what separates APIs that remain secure as they scale from APIs that generate breach incidents.
Related Articles
MLOps Guide: Taking Machine Learning Models to Production [2026]
87% of machine learning models built by data science teams never reach production. The models work — they pass cross-validation, they score well on holdout sets, they demonstrate genuine predictive value. The problem is not the modeling. The problem is everything that happens between a notebook experiment and a reliable, monitored, production system. MLOps is the discipline that closes that gap. This guide covers the full MLOps stack: maturity levels, tooling choices (MLflow, DVC, Kubeflow
Read More: MLOps Guide: Taking Machine Learning Models to Production [2026]LLM Fine-Tuning Guide: Custom Model Training with LoRA and QLoRA [2026]
General-purpose LLMs are impressive. They can write code, summarize documents, answer questions, and translate between languages with reasonable accuracy. But "reasonable" is not good enough when your application requires consistent output format, domain-specific terminology, a particular tone, or behavior that the base model was never trained to exhibit. That gap is where fine-tuning matters. Fine-tuning updates a model's weights on your specific data, changing how the model behaves — not
Read More: LLM Fine-Tuning Guide: Custom Model Training with LoRA and QLoRA [2026]Computer Vision Applications: Object Detection, OCR, and Industrial AI [2026]
Computer vision has moved well past the research phase. The models are trained, the frameworks are mature, the hardware is accessible, and the use cases are generating measurable returns. What was a specialized capability requiring deep expertise in 2018 is now deployable infrastructure — if you know which component to reach for and where the real complexity lives. This guide covers computer vision applications across industrial, medical, logistics, and document processing domains. It expl
Read More: Computer Vision Applications: Object Detection, OCR, and Industrial AI [2026]
