Api Rate Abuse in Nestjs with Openid Connect
Api Rate Abuse in Nestjs with Openid Connect — how this specific combination creates or exposes the vulnerability
Rate abuse in a NestJS API that uses OpenID Connect (OIDC) often arises from mismatched trust boundaries and missing granular enforcement. OIDC adds identity and scopes, but if rate limiting is applied only after authentication or relies on claims that can be manipulated, attackers can bypass intended limits.
Consider an endpoint that relies on an OIDC access token for authorization but does not enforce rate limits on the unauthenticated path to the introspection or userinfo endpoints. An attacker can flood these endpoints with token validation requests, consuming server resources and potentially degrading availability. Even when tokens are validated, if rate limits are keyed only by client IP, a shared proxy or load balancer can cause false negatives, allowing abuse to slip through.
Within NestJS, a common pattern is to use guards that verify the OIDC token and attach user details to the request. If rate limiting is implemented as a global guard or middleware that runs after authentication, an authenticated but abusive client can still make excessive requests because limits are applied per authenticated identity rather than per originating factor. Identities issued by an OIDC provider can be rotated or spoofed in certain configurations, especially if nonce and issuer validation are not strictly enforced, enabling attackers to generate new identities to evade per-identity rate caps.
Another specific vector involves scopes and claims. If your rate limiting logic incorrectly trusts a scope claim to determine request quotas, an attacker who obtains a token with high-privilege scopes may exceed intended operation limits. For example, a token with scope 'read write' might be used to call rate-sensitive endpoints at a higher volume than intended, because the limit was tied to scope rather than to a verified, non-repudiable client identifier like the OIDC client_id combined with a server-side nonce or sid claim.
OpenID Connect configuration in NestJS often involves libraries that validate signatures and claims. If you do not enforce strict issuer validation (iss), require a nonce for implicit flows, or validate the at_hash for access tokens where applicable, tokens can be substituted or replayed, undermining rate limiting that depends on token integrity. Additionally, if you accept tokens without checking the acr (authentication context class) or without ensuring the token is bound to the intended resource server, an attacker might reuse tokens across services, each with its own rate limits, effectively multiplying abuse potential.
Operational impacts include increased CPU usage from signature verification and introspection calls, inflated logs from token validation attempts, and degraded response times for legitimate users. Because OIDC introduces multiple identity-bound tokens and optional claims, naive rate limiting strategies can inadvertently permit abuse paths that are not present in simpler API key models. Hardening requires aligning rate limits with verifiable, immutable client and session identifiers, validating tokens rigorously, and monitoring for token reuse patterns that indicate credential abuse.
Openid Connect-Specific Remediation in Nestjs — concrete code fixes
To secure NestJS APIs with OpenID Connect while mitigating rate abuse, combine strict OIDC validation with rate limiting strategies that use trusted, immutable identifiers. Below are concrete practices and code examples.
1. Strict OIDC configuration with issuer and nonce validation
Ensure your OIDC setup validates issuer, audience, and nonce. Use a well-maintained library and avoid permissive settings.
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { Issuer, generators } from 'oidc-provider';
// Example configuration concept for a NestJS OIDC client integration
// This represents how you should enforce strict validation on the client side.
// In practice, you would use a library like @nestjs/passport with openid-client.
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-openidconnect';
@Injectable()
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
constructor() {
super({
issuer: 'https://accounts.google.com', // or your IdP
authorizationURL: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenURL: 'https://oauth2.googleapis.com/token',
userInfoURL: 'https://openidconnect.googleapis.com/v1/userinfo',
clientID: process.env.OIDC_CLIENT_ID,
clientSecret: process.env.OIDC_CLIENT_SECRET,
callbackURL: 'https://api.example.com/auth/callback',
scope: ['openid', 'profile', 'email'],
nonce: generators.nonce(),
// Enforce strict host and issuer checks
passReqToCallback: true,
});
}
async validate(req: Request, profile: any, done: VerifyCallback) {
// Additional checks: verify nonce and iss
if (profile.nonce !== req.session?.expectedNonce) {
return done(new Error('Invalid nonce'), false);
}
// Ensure iss matches expected issuer
if (profile.issuer !== 'https://accounts.google.com') {
return done(new Error('Invalid issuer'), false);
}
return done(null, profile);
}
}
2. Rate limiting keyed by verified client and session identifiers
Use a combination of client_id and a stable session or nonce to create rate limit keys. Avoid relying solely on IP or unverified claims.
import { Injectable } from '@nestjs/common';
import { RateLimiterRedis } from 'rate-limiter-flexible';
@Injectable()
export class RateLimitService {
private rateLimiter: RateLimiterRedis;
constructor() {
this.rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'middlebrick_oidc_rate_limit',
points: 100, // 100 requests
duration: 60, // per 60 seconds
});
}
async consumeByOidcIdentity(clientId: string, sessionId: string): Promise {
// Use clientId and sessionId (or nonce/sid) to ensure uniqueness
const key = `${clientId}:${sessionId}`;
try {
await this.rateLimiter.consume(key);
} catch (rejRes) {
throw new HttpException('Too many requests', 429);
}
}
}
3. Validate token binding and prevent reuse
Track at_hash for access tokens where provided and monitor sid (session ID) to detect token reuse across services. Reject tokens that do not meet expected binding criteria.
// Example token validation hook after passport verification
app.use((req: Request, res: Response, next: NextFunction) => {
const user = req.user;
const accessToken = req.headers.authorization?.split(' ')[1];
if (user && accessToken) {
// Verify at_hash if present in token claims
const atHash = getAtHashFromToken(accessToken);
if (!validateAtHash(accessToken, atHash)) {
return next(new Error('Token binding mismatch'));
}
// Optionally record sid to detect reuse
trackSessionId(user.sid);
}
next();
});
4. Layered rate limits: unauth path and introspection
Apply stricter limits to token validation and userinfo endpoints to prevent probing attacks. Use separate rate limiters for unauthenticated discovery paths and authenticated operations.
// Apply different rate limiters to specific routes
const strictLimiter = new RateLimiterRedis({ points: 10, duration: 60 });
const moderateLimiter = new RateLimiterRedis({ points: 300, duration: 60 });
app.get('/.well-known/openid-configuration', (req, res, next) => strictLimiter.consume(req.ip).then(next).catch(() => res.status(429).send('Too many requests'));
app.get('/userinfo', oidcAuthenticate, (req, res, next) => moderateLimiter.consume(req.user.clientId).then(next).catch(() => res.status(429).send('Too many requests'));
5. Continuous monitoring and claim-based policies
Log nonce, iss, and sid values to detect anomalies. Use claims to apply policy but never as the sole rate-limiting key.
// Logging OIDC context for audit and anomaly detection
logger.info('OIDC token used', {
clientId: req.user.client_id,
iss: req.user.iss,
nonce: req.user.nonce,
sid: req.user.sid,
scope: req.user.scope,
});