Why Identity APIs Are Your Highest-Value Target
Every identity system exposes APIs—for authentication, token issuance, user management, and provisioning. These APIs are fundamentally different from your average CRUD endpoint because a single vulnerability can compromise your entire user base. A broken authentication endpoint does not leak one record; it leaks the keys to everything.
The OWASP API Security Top 10 consistently ranks Broken Object Level Authorization (BOLA), Broken Authentication, and Broken Function Level Authorization as the top threats. In identity systems, these risks are amplified because the objects being accessed are the authorization data itself.
This guide covers the architectural patterns and implementation details that protect identity APIs in modern cloud-native environments.
API Security Architecture
A well-designed identity API security architecture operates in layers. No single control is sufficient—defense in depth is essential.
OAuth Token Validation: Getting It Right
Token validation is the most critical security function in your API layer. A mistake here means unauthorized access to your entire system. Here is a production-grade JWT validation implementation:
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
// Configure JWKS client with caching
const client = jwksClient({
jwksUri: 'https://idp.example.com/.well-known/jwks.json',
cache: true,
cacheMaxEntries: 5,
cacheMaxAge: 600000, // 10 minutes
rateLimit: true,
jwksRequestsPerMinute: 10,
});
function getSigningKey(header: jwt.JwtHeader): Promise<string> {
return new Promise((resolve, reject) => {
client.getSigningKey(header.kid, (err, key) => {
if (err) return reject(err);
resolve(key!.getPublicKey());
});
});
}
interface TokenValidationOptions {
audience: string;
issuer: string;
requiredScopes?: string[];
}
async function validateAccessToken(
token: string,
options: TokenValidationOptions
): Promise<jwt.JwtPayload> {
// Step 1: Decode header WITHOUT verification to get kid
const decoded = jwt.decode(token, { complete: true });
if (!decoded || typeof decoded === 'string') {
throw new Error('Invalid token format');
}
// Step 2: Reject 'none' algorithm — critical security check
if (decoded.header.alg === 'none') {
throw new Error('Algorithm "none" is not allowed');
}
// Step 3: Only allow expected algorithms
const allowedAlgorithms: jwt.Algorithm[] = ['RS256', 'ES256'];
if (!allowedAlgorithms.includes(decoded.header.alg as jwt.Algorithm)) {
throw new Error(Algorithm ${decoded.header.alg} is not allowed);
}
// Step 4: Get the signing key
const signingKey = await getSigningKey(decoded.header);
// Step 5: Verify signature, expiration, audience, issuer
const payload = jwt.verify(token, signingKey, {
algorithms: allowedAlgorithms,
audience: options.audience,
issuer: options.issuer,
clockTolerance: 30, // 30 seconds clock skew tolerance
}) as jwt.JwtPayload;
// Step 6: Validate required scopes
if (options.requiredScopes?.length) {
const tokenScopes = (payload.scope || '').split(' ');
const hasAllScopes = options.requiredScopes.every(
(scope) => tokenScopes.includes(scope)
);
if (!hasAllScopes) {
throw new Error('Insufficient scope');
}
}
return payload;
}
Common JWT Validation Mistakes
- Not validating the
audclaim: If you skip audience validation, a token issued for Service A can be used to access Service B. Always validate. - Accepting the
algfrom the token header blindly: This enables algorithm confusion attacks. Maintain a server-side allowlist. - Not checking token revocation: JWTs are stateless by design, but you still need a revocation mechanism for logout and compromise scenarios. Check a revocation list or use short-lived tokens with refresh token rotation.
- Leaking tokens in URLs: Never pass tokens as query parameters—they end up in server logs, browser history, and referer headers.
Rate Limiting for Identity Endpoints
Identity endpoints are particularly sensitive to abuse. Login endpoints face credential stuffing. Token endpoints face brute force. User enumeration endpoints face scraping. Here is a tiered rate limiting strategy:
import { RateLimiterRedis } from 'rate-limiter-flexible';
import Redis from 'ioredis';
const redis = new Redis({ host: 'redis-cluster', port: 6379 });
// Tier 1: Global rate limit — protects against DDoS
const globalLimiter = new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'rl:global',
points: 1000, // 1000 requests
duration: 1, // per second
blockDuration: 60, // block for 60s if exceeded
});
// Tier 2: Per-IP rate limit — protects against credential stuffing
const ipLimiter = new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'rl:ip',
points: 20, // 20 requests
duration: 60, // per minute
blockDuration: 300, // block for 5 minutes
});
// Tier 3: Per-user rate limit on auth endpoints — brute force protection
const authLimiter = new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'rl:auth',
points: 5, // 5 attempts
duration: 900, // per 15 minutes
blockDuration: 900, // block for 15 minutes
});
async function rateLimitMiddleware(req, res, next) {
try {
// Always apply global limit
await globalLimiter.consume('global');
// Apply IP limit
await ipLimiter.consume(req.ip);
// For auth endpoints, apply stricter per-user limit
if (req.path.startsWith('/auth/') && req.body?.username) {
await authLimiter.consume(req.body.username);
}
// Set rate limit headers for client visibility
const ipResult = await ipLimiter.get(req.ip);
res.set({
'X-RateLimit-Limit': '20',
'X-RateLimit-Remaining': String(
Math.max(0, 20 - (ipResult?.consumedPoints || 0))
),
'X-RateLimit-Reset': String(
Math.ceil((ipResult?.msBeforeNext || 0) / 1000)
),
});
next();
} catch (rateLimitError) {
res.status(429).json({
error: 'rate_limit_exceeded',
message: 'Too many requests. Please try again later.',
retryAfter: Math.ceil(
(rateLimitError.msBeforeNext || 60000) / 1000
),
});
}
}
Rate Limiting Anti-Patterns
- Rate limiting only by IP: Attackers use distributed botnets with thousands of IPs. Combine IP-based limits with user-based and global limits.
- Identical limits for all endpoints: Login and token endpoints should have much stricter limits than read-only user profile endpoints.
- No backoff signaling: Always return
Retry-Afterheaders so well-behaved clients can back off gracefully.
Defending Against the OWASP API Top 10
BOLA (Broken Object Level Authorization)
BOLA is the #1 API vulnerability. In identity APIs, it manifests when a user can access another user's profile, roles, or tokens by manipulating object IDs.
// VULNERABLE — no ownership check
app.get('/api/users/:userId/sessions', async (req, res) => {
const sessions = await db.sessions.find({ userId: req.params.userId });
return res.json(sessions);
});
// SECURE — validate the authenticated user owns the resource
app.get('/api/users/:userId/sessions', authenticate, async (req, res) => {
// req.auth.sub comes from validated JWT
if (req.auth.sub !== req.params.userId && !req.auth.roles.includes('admin')) {
return res.status(403).json({ error: 'forbidden' });
}
const sessions = await db.sessions.find({ userId: req.params.userId });
return res.json(sessions);
});
Broken Authentication
For identity APIs specifically, broken authentication often means:
- Missing MFA enforcement on sensitive operations: Changing email, resetting passwords, or modifying OAuth scopes should require step-up authentication.
- Weak token generation: Use cryptographically secure random number generators for authorization codes and refresh tokens.
- No token binding: Bind refresh tokens to the client fingerprint (device, IP range) to limit token theft impact.
Mass Assignment
Identity APIs often expose user profile update endpoints. Without proper field filtering, attackers can set fields they should not control:
// VULNERABLE — accepts any field from the request body
app.patch('/api/users/:id', authenticate, async (req, res) => {
await db.users.update(req.params.id, req.body);
// Attacker sends: { "role": "admin", "emailVerified": true }
});
// SECURE — allowlist of updatable fields
app.patch('/api/users/:id', authenticate, async (req, res) => {
const allowedFields = ['displayName', 'avatarUrl', 'timezone'];
const updates = {};
for (const field of allowedFields) {
if (req.body[field] !== undefined) {
updates[field] = req.body[field];
}
}
await db.users.update(req.params.id, updates);
});
Input Validation for Identity Data
Identity data has specific validation requirements that generic input validation misses:
import { z } from 'zod';
const loginSchema = z.object({
email: z.string()
.email('Invalid email format')
.max(254, 'Email too long') // RFC 5321 limit
.toLowerCase() // Normalize
.trim(),
password: z.string()
.min(8, 'Password too short')
.max(128, 'Password too long'), // Prevent DoS via bcrypt
// Never accept redirect URIs from user input without validation
redirectUri: z.string()
.url()
.refine(
(uri) => ALLOWED_REDIRECT_URIS.includes(uri),
'Invalid redirect URI'
)
.optional(),
});
const scopeRequestSchema = z.object({
scope: z.string()
.transform((s) => s.split(' '))
.pipe(
z.array(
z.enum(['openid', 'profile', 'email', 'read:users', 'write:users'])
)
),
});
Redirect URI Validation
Open redirect vulnerabilities in OAuth flows are particularly dangerous because they enable authorization code interception. Validate redirect URIs against an exact-match allowlist—never use prefix matching or regex.
API Gateway Patterns for Identity
The API gateway is your primary enforcement point. Here are patterns specific to identity APIs:
Token Exchange at the Gateway
In a Zero Trust architecture (see Zero Trust Networks), the gateway should exchange external tokens for internal tokens with different scopes and audiences:
# Kong / API Gateway configuration example
plugins:
- name: jwt
config:
claims_to_verify:
- exp
- iss
allowed_iss:
- https://idp.example.com
run_on_preflight: false
- name: rate-limiting
config:
minute: 30
hour: 500
policy: redis
redis_host: redis-cluster
- name: request-transformer
config:
remove:
headers:
- Authorization # Strip external token
add:
headers:
- "X-Internal-User-Id:$(jwt.sub)"
- "X-Internal-Scopes:$(jwt.scope)"
Scope-Based Routing
Route requests to different backend services based on OAuth scopes in the token. This enforces least privilege at the network level:
- Tokens with
read:usersscope → read-replica user service - Tokens with
write:usersscope → primary user service (with additional authorization) - Tokens with
admin:*scope → admin service behind additional authentication
Monitoring and Anomaly Detection
Identity APIs require specific monitoring signals beyond standard APM:
- Failed authentication rate by IP, user, and client: Spikes indicate credential stuffing or brute force
- Token issuance rate: Unusual spikes may indicate compromised client credentials
- Scope escalation attempts: Requests for scopes beyond what was granted indicate probing
- Geographic anomalies: A user authenticating from two continents within minutes (implement risk-based authentication)
- Unusual API access patterns: A user suddenly accessing hundreds of other user profiles (BOLA exploitation)
// Example: structured audit log for identity events
function logIdentityEvent(event: {
action: string;
userId?: string;
clientId?: string;
ip: string;
userAgent: string;
result: 'success' | 'failure';
reason?: string;
riskScore?: number;
}) {
logger.info({
...event,
timestamp: new Date().toISOString(),
service: 'identity-api',
// Include correlation ID for distributed tracing
correlationId: asyncLocalStorage.getStore()?.correlationId,
});
}
Checklist: Securing Your Identity APIs
Before deploying identity APIs to production, validate each of these controls:
- Transport: TLS 1.3 only, HSTS enabled, certificate pinning for mobile clients
- Authentication: OAuth 2.0 with PKCE for public clients, client credentials with mutual TLS for service-to-service
- Authorization: RBAC or ABAC at every endpoint, object-level access control on all resource endpoints
- Rate limiting: Tiered limits (global, per-IP, per-user, per-endpoint)
- Input validation: Schema validation on all inputs, redirect URI allowlisting, output encoding
- Token security: Short-lived access tokens (5-15 min), refresh token rotation, revocation endpoint
- Logging: Structured audit logs for all auth events, no PII or tokens in logs
- Testing: Regular penetration testing, automated OWASP ZAP scans in CI/CD