Api Rate Abuse in Fastapi with Openid Connect
Api Rate Abuse in Fastapi with Openid Connect — how this specific combination creates or exposes the vulnerability
Rate abuse in FastAPI when OpenID Connect (OIDC) is used for authentication can occur when rate limiting is applied after authentication or is bypassed by token handling choices. In this combination, an attacker may obtain a valid ID token and use it to make authenticated requests, potentially evading IP-based limits if the limit is applied per IP rather than per authenticated identity. If the FastAPI application applies rate limits only on unauthenticated paths or relies on client-supplied claims without verification, authenticated endpoints can be hammered using stolen or legitimate tokens.
Consider a FastAPI app that uses an OIDC provider (e.g., Auth0, Okta, or a self-hosted Keycloak) and decorates routes with a dependency that validates an access token. If the rate limit is implemented as a simple in-memory counter keyed by request IP, an attacker with a valid token can rotate IPs (via proxy or botnet) or use a single token to exceed intended limits. Conversely, if limits are applied too early in the middleware stack, they might not consider token validity, allowing abusive traffic to consume authentication and introspection resources.
Another subtle risk arises when token introspection or userinfo calls are triggered on each request; an attacker can cause high load on the OIDC provider by flooding endpoints with valid tokens, effectively combining authentication load with rate bypass. The OWASP API Top 10 category for Security Misconfiguration and the BOLA/IDOR checks highlight the importance of scoping and authorization controls, which can be undermined if rate limits do not account for authenticated identities.
In practice, this manifests as increased latency, higher CPU usage, or quota exhaustion for legitimate users when the OIDC validation path becomes a bottleneck. For example, an endpoint that returns sensitive user data without proper scope or role checks can be called repeatedly using a valid token, bypassing coarse IP-level protections. The risk is elevated when token validation is performed per request without caching nonces or introspection results, and when the application does not enforce per-identity rate limits.
Openid Connect-Specific Remediation in Fastapi — concrete code fixes
To mitigate rate abuse in FastAPI with OpenID Connect, combine per-identity rate limiting with robust token validation and caching. Use a sliding window or token bucket algorithm keyed by a stable subject claim (e.g., sub) rather than IP. Ensure token validation is performed once per request and results are reused within a short window to reduce load on the OIDC provider.
Example FastAPI setup with OIDC validation using python-jose and httpx, followed by per-sub rate limiting using a Redis-backed algorithm:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError
import httpx
import aioredis
import time
app = FastAPI()
security = HTTPBearer()
# OIDC configuration (replace with your provider values)
OIDC_CONFIG = {
"issuer": "https://auth.example.com/",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"audience": "api.example.com",
}
redis = None # initialized in lifespan
def get_public_key(kid: str) -> str:
# Simplified JWKS fetch and key selection; use caching in production
resp = httpx.get(OIDC_CONFIG["jwks_uri"])
resp.raise_for_status()
jwks = resp.json()
for key in jwks.get("keys", []):
if key["kid"] == kid:
return key
raise ValueError("key not found")
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> dict:
token = credentials.credentials
try:
# In production, cache the decoded header to fetch keys less often
from jose.utils import base64url_decode
import json
header = jwt.get_unverified_header(token)
key = get_public_key(header["kid"])
claims = jwt.decode(
token,
key=key,
algorithms=["RS256"],
audience=OIDC_CONFIG["audience"],
issuer=OIDC_CONFIG["issuer"],
)
claims.validate() # raises on expired, etc.
return claims
except JWTError as e:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
async def limiter(subject: str = Depends(lambda c: c["sub"])) -> None:
if not redis:
return
now = int(time.time())
window = 60 # 1 minute sliding window
key = f"rate:{subject}"
# Simple sliding window using sorted sets
await redis.zadd(key, {str(now): now})
await redis.zremrangebyscore(key, 0, now - window)
count = await redis.zcount(key, now - window, now)
if count > 100: # allow 100 requests per minute per subject
raise HTTPException(status_code=429, detail="Rate limit exceeded")
@app.post("/data")
async def get_data(
user: dict = Depends(verify_token),
_: None = Depends(limiter),
):
return {"user_id": user["sub"], "data": "protected"}
# lifespan to initialize redis (FastAPI 0.103+)
@app.on_event("startup")
async def startup():
global redis
redis = await aioredis.from_url("redis://localhost")
@app.on_event("shutdown")
async def shutdown():
if redis:
await redis.close()
Key points in this remediation:
- Rate limiting is applied per authenticated subject (sub), not IP, preventing attackers from rotating IPs to bypass limits.
- Token validation is explicit and errors result in 401, ensuring malformed or expired tokens do not consume excessive resources.
- Redis-backed sliding window provides a consistent, distributed rate limit across instances.
- By validating tokens and reusing claims within a request, you reduce redundant introspection calls that could be abused.
If your stack uses a managed identity platform, you can replace the manual JWT validation with an OIDC client library that handles JWKS caching and signature verification, but always enforce server-side rate limits on the identity subject.