API Security, Authentication and Authorization Questions
Controlling who can call an API, what they may do, and defending it against abuse. Covers the access-control mechanics: API keys, OAuth 2.0 flows, OpenID Connect, JWT issuance/validation, session vs. token auth, scopes/roles for fine-grained authorization, token lifetime and refresh, mutual TLS, and machine-to-machine vs. user-delegated access. Also covers the adversarial hardening view: input validation, injection and deserialization risks, broken object-level authorization (BOLA), mass assignment, secrets handling, and the OWASP API Security Top 10, plus securing data in transit, preventing enumeration/scraping, and testing APIs for vulnerabilities.
Given a GraphQL mutation that accepts deeply nested input to create users and related resources, perform a threat model that focuses on injection, excessive data exposure, denial-of-service via complex nested queries, and authorization bypass. Propose precise mitigations such as sanitization, field-level authorization hooks, depth/complexity limiting, persisted queries and cost estimation.
Sample Answer
Direct answer
For a GraphQL mutation that accepts deeply nested input to create users and related resources,
the threat model has four connected risks: injection through any field that reaches a data
store or downstream system, excessive data exposure through the response shape a single query
can request, denial-of-service through the cost of resolving deeply nested or high-fan-out
selections, and authorization bypass at the object and field level within the nested structure,
not just at the mutation's top-level entry point. Because a single GraphQL request can touch
many resources and relationships in one call, each of these risks compounds with nesting depth
in a way a single flat REST endpoint does not.
Structured elaboration
Injection. Every leaf value in the nested input (a related resource's name, an address
field several levels deep) is still untrusted input reaching business logic and, eventually,
storage; the nesting does not change the injection risk, it just multiplies how many fields need
the same discipline applied consistently: parameterized queries, never string-built ones, plus
strict schema validation on every nested object (not only the mutation's top-level arguments,
which is where a reviewer's eye is naturally drawn) and correct output encoding wherever any of
this data is later rendered back out.
Excessive data exposure. Because the same mutation can request a return shape that includes
newly created and related resources, a response can overfetch: it can return internal or
sensitive fields on those related resources that the caller was never meant to see, simply
because the schema allows selecting them and nobody scoped the mutation's response shape as
carefully as its input shape was scoped. Mitigate this the same way field-level authorization
is enforced on queries: role- and ownership-aware field resolvers on the response type, not an
assumption that "this is a write endpoint, so read-side authorization doesn't apply here."
Denial-of-service via complex nested queries. A mutation with deeply nested input, or a
follow-up query selecting deeply nested relationships, can force the server to do
exponentially more work than the request's size on the wire suggests, since each level of
nesting can multiply the resolvers invoked by the requested page size at that level. Mitigate
with depth limiting (rejecting a query or mutation whose selection nests deeper than a configured
maximum) and query complexity/cost analysis (assigning each field a cost, multiplying by
requested list sizes at each level, and rejecting a request whose total cost exceeds a budget
before executing any of it).
Authorization bypass. A nested mutation creating multiple related resources in one call needs
authorization checked at every object being created or referenced, not only at the top-level
"can this user call this mutation at all" gate; a caller authorized to create their own user
profile is not automatically authorized to attach that profile to an organization they do not
belong to, simply because the organization reference is buried three levels into the nested
input rather than being the mutation's top-level argument.
Concrete mitigations, tied to the risks above:
- Sanitization and parameterization at every leaf field, same discipline as any other
user-supplied input reaching a data store. - Field-level authorization hooks on both the input side (can this caller set this field or
reference this related object) and the response side (can this caller see this field on the
result), not only on the mutation as a whole. - Depth and complexity limiting, rejecting requests that exceed a configured nesting depth
or computed cost before execution begins, so a malicious request is rejected cheaply rather
than partially executed before being caught. - Persisted queries (the client sends a reference to a pre-registered, pre-approved query
shape rather than an arbitrary ad-hoc query string), which is a strong mitigation against
unexpected malicious query shapes specifically, since the server only ever executes shapes it
already reviewed and approved, though it does not by itself replace per-request authorization
checks on the data those approved shapes touch. - Cost estimation, the mechanism underlying complexity limiting: assign a numeric cost to
each field (higher for fields that fan out via a list) so the total cost of a specific request
can be computed and compared against a budget before execution.
Operational testing to validate the mitigations. These protections need to be verified the
same way any other security control is verified, with automated tests exercising the negative
cases: a test that submits a mutation nested one level past the configured depth limit and
asserts it is rejected before any database write occurs; a test computing a request's expected
cost against the cost model and asserting the server's rejection threshold matches; and a
field-level authorization test that runs the identical nested mutation as two different callers
(one entitled to set or see a given nested field or object, one not) and asserts the
unauthorized caller's attempt is rejected or the field comes back redacted, since a passing
depth-limit test says nothing about whether the authorization checks inside that depth are
actually being enforced.
Overfetching and introspection abuse, as related but distinct concerns. Overfetching (a
client requesting far more of the graph than its use case needs, even without malicious intent)
is a milder version of the excessive-data-exposure risk above and is best addressed by the same
field-level authorization plus reasonable default response shapes; introspection abuse (an
attacker using the schema's own introspection query to map out the entire graph, including
fields or types not intended for public discovery, as reconnaissance for a later attack) is
mitigated by disabling introspection in production, backed by a checked-in schema snapshot so
tooling and tests still have a field list to work from, or, where some introspection must stay
available, at minimum excluding internal-only types and fields from it.
Worked example
Consider a nested mutation: user { id, posts(first: 20) { id, comments(first: 20) { id } } }.
Using a simple cost model where a scalar field costs 1 point per row in scope and a list field
multiplies the cost of everything beneath it by the requested page size:
user.id : 1 (scope multiplier 1)
user.posts (node) : 1 (scope multiplier 1, before its own multiplier applies)
posts.id : 20 (scope multiplier 20, from posts' page size)
posts.comments (node) : 20 (scope multiplier 20)
comments.id : 400 (scope multiplier 20 * 20, nested page sizes compound)
-----------------------------------------------
total cost : 442
If the server enforces a cost budget of, say, 300 per request, this query is rejected before
execution, purely from its declared shape, with no database call made. Without complexity
limiting, a client (or attacker) could push first: 100 at each level instead of first: 20,
pushing the comments.id term alone to 100 * 100 = 10,000, a strictly wire-cheap request that
would force the server to resolve tens of thousands of rows.
Trade-offs and pitfalls
- Depth and complexity limits are blunt instruments that can reject legitimate, unusually
shaped requests, not just malicious ones; calibrate the budget against your real schema's
legitimate worst-case use cases, and expose a documented way for a legitimate client with a
genuinely larger need to request a higher limit, the same way an API rate-limit exception
process should exist. - Persisted queries strongly constrain query shape but do nothing about authorization on the
data a shape touches; a persisted query approved months ago can still be misused by a
caller who should not have access to the specific objects it happens to reference this time, so
persisted queries reduce, but do not replace, per-request field- and object-level authorization. - Common wrong turn: authorizing only the mutation's top-level entry point. A nested mutation
is not one flat authorization check, it is potentially many, one per object being created or
referenced within the nested input, and skipping the nested ones is exactly the authorization-
bypass risk this threat model calls out. - Common wrong turn: validating shape (depth, complexity) but never testing it. A depth limit
that was implemented but never covered by a test asserting it actually rejects an over-depth
request is a control that looks present in code review and absent in practice the first time it
matters.
You need an access control model for an API that supports fine-grained permissions (resource-level, action-level) and can scale to millions of principals and resources. Discuss evaluation latency, caching of permissions, hierarchical roles, attribute-based access control, and how to keep revocation latency low.
Sample Answer
Direct answer
Separate the authorization decision from the authorization data: build a dedicated policy-evaluation layer that answers "can this principal take this action on this resource" against a cached, denormalized view of the permission graph, rather than computing that answer with a live join across primary relational tables on every request. At millions of principals and resources, the live-join approach is both too slow and too tightly coupled to core application schema.
Structured elaboration
Modeling resource-level and action-level permissions
Model permissions as tuples of principal, action, and resource, or principal, action, and resource pattern, rather than a single flat role per user. Real APIs need both "can Alice read document 42," resource-level and specific, and "can any Editor create documents in project X," action-level and pattern-based. A model that can only express one of these two shapes forces awkward workarounds for the other.
Hierarchical roles
Define roles that inherit from other roles, Viewer as a subset of Editor as a subset of Admin, so permission grants are authored once per role rather than once per user-permission pair. Combine this with resource hierarchies, a permission granted at a folder or project level implicitly applying to everything nested underneath, so you are not writing millions of individual grants for millions of individual resources.
Attribute-based access control (ABAC)
Layer attribute-based rules on top of the role hierarchy for decisions that depend on context a static role cannot express: time of day, a resource's sensitivity tag, whether a principal's department matches the resource's owning department, or the request's originating IP range. The practical pattern at scale is "roles for the coarse default, ABAC for the exceptions," not replacing roles entirely; a pure-ABAC system where every decision is a fresh rule evaluation becomes far harder to reason about and audit as the rule set grows.
Evaluation latency
The authorization check sits directly on the request hot path, so it has to be fast. Achieve that by pre-computing or denormalizing the parts of the decision that do not change often, role-to-permission mappings and resource hierarchy, into a form the evaluation layer can check in memory or via a single cache lookup, reserving genuinely dynamic per-request evaluation for the smaller set of cases that actually need ABAC attribute matching.
Caching of permissions
Cache the evaluated decision, or a principal's effective permission set, close to the service making the check, with a short time-to-live (TTL). Cache the underlying role and hierarchy graph, which changes far less often than individual grants, more aggressively. These two caches need different invalidation strategies, since they change at very different rates.
Keeping revocation latency low
This is the direct tension with caching: a cached "yes" that should now be "no" is a live authorization bug, not merely staleness, so revocation needs an active invalidation path, pushing a targeted cache-bust for the specific principal, resource, or role that changed, rather than relying on TTL expiry alone to eventually catch up. A common pattern combines a short default TTL, seconds, not minutes, with an active invalidation event fired on any grant or role change, so the cache stays fresh in the common case and the invalidation event closes the remaining gap immediately rather than waiting out the TTL.
Worked example
A document-sharing API has a folder hierarchy. Granting "Editor" on a top-level folder to a principal applies to every document created under it going forward, with no new grant row written per document. The evaluation layer first checks the principal's cached effective-permission set, the fast path, doing no real work at all on a cache hit. On a miss, it walks the resource's hierarchy chain upward to the nearest ancestor with an explicit grant, then caches that result with a short TTL. Revoking the folder-level grant fires an invalidation event that busts the cached decision for every principal-resource pair touched by that grant, rather than waiting for each individual cache entry's TTL to expire on its own.
Trade-offs & pitfalls
Hierarchical inheritance is powerful but makes "why does this principal have this permission" hard to answer without good tooling; a permission-explain or trace capability is not optional at this scale, it is how the system actually gets debugged and audited. ABAC's flexibility is also its risk: rules that reference many attributes become hard to reason about and can interact in unintended ways, so keep the ABAC rule set small and reviewed rather than letting it grow into an ever-larger, ever-harder-to-audit pile. Treating revocation latency as "eventually consistent is fine" is the single most common mistake at this scale; a stale cache that grants access after it should have been revoked is a security incident, not a minor user-experience issue, so the invalidation path deserves as much engineering attention as the fast-path cache itself.
Define rate limiting, throttling, and quotas in the context of APIs. Describe the token-bucket, leaky-bucket and fixed-window algorithms, and explain practical strategies for per-user, per-IP, per-client and global limits, as well as handling bursty traffic and fairness.
Sample Answer
Direct answer
Rate limiting caps how many requests a caller can make in a given window; throttling is the
mechanism that enforces that cap by delaying or rejecting requests once it is reached; a quota is
a longer-horizon budget (often daily or monthly) layered on top, separate from a short-window
rate limit. The three classic algorithms, token bucket, leaky bucket, and fixed window, differ
mainly in how they treat bursts of traffic that arrive faster than the sustained allowed rate,
which matters because most real traffic is not smooth, it is bursty.
Structured elaboration
Fixed window. Count requests in discrete, non-overlapping time windows (for example, one
counter that resets every 60 seconds) and reject once the count exceeds the limit. It is the
simplest to implement and reason about, but has a well-known boundary flaw: a client can send a
full window's worth of requests right at the end of one window and another full window's worth
immediately at the start of the next, doubling the effective burst right at the boundary (worked
out below).
Token bucket. A bucket holds up to burst tokens and refills continuously at
tokens_per_sec; each request consumes one token, and a request is rejected only once the
bucket is empty. This naturally allows a burst up to the bucket's capacity while still enforcing
a long-run average rate equal to the refill rate, which is usually the closest match to how real
clients behave (idle, then a burst of activity).
Leaky bucket. Modeled as a fixed-size queue that is filled by incoming requests and drained
(processed) at a constant rate; if the queue is full, new requests are rejected. Where token
bucket allows a burst to pass through immediately (up to the bucket size), leaky bucket smooths
a burst out into a steady output rate, at the cost of adding queuing delay to the requests that
arrive during the burst.
Per-user, per-IP, per-client, and global limits. These are not alternatives to each other,
they are usually layered: a global limit protects the backend from aggregate overload regardless
of source; a per-client (per API key) limit is the primary fairness mechanism between distinct
customers; a per-user limit protects against one compromised or misbehaving account inside a
larger client; a per-IP limit adds a cheap first line of defense against anonymous or
unauthenticated abuse, understanding that many real users can share one IP (a corporate NAT), so
per-IP limits alone are a blunt fairness tool.
Bursty traffic and fairness. Token bucket's burst parameter is the direct knob for how much
burst capacity to tolerate before the sustained rate kicks in; setting it too low makes normal,
slightly bursty legitimate usage feel throttled, and setting it too high defeats the point of
having a sustained rate limit at all. Fairness across many keys typically means giving each key
its own independent bucket or counter (as in the earlier rate-limiter implementation on this
topic) rather than one shared counter that lets one noisy client starve everyone else's share of
capacity.
Worked example
Fixed window's boundary flaw, with real numbers. Limit: 100 requests per 60-second window.
Window 1 spans [0:00, 1:00); window 2 spans [1:00, 2:00). A client sends 100 requests at 0:59.5
(all counted in window 1, at the limit) and another 100 requests at 1:00.5 (all counted in
window 2, also at the limit). Both windows individually respect the 100-per-minute cap, yet 200
requests passed within the single one-second span from 0:59.5 to 1:00.5, double the intended
rate concentrated right at the boundary.
Token bucket, with real numbers. burst = 20, tokens_per_sec = 5. A client that has been
idle can immediately send 20 requests back to back (draining the bucket), matching the intended
burst tolerance. After the bucket is empty, tokens refill one every 1 / 5 = 0.2 seconds, so a
sustained caller settles into exactly 5 requests/second, matching the configured rate, with no
further burst until idle time lets the bucket refill.
Leaky bucket, with real numbers. Queue capacity 20, drain rate 5 requests/second. If 20
requests arrive simultaneously, the queue fills completely and the 20th request is not rejected,
but it is not processed immediately either: it is dequeued and processed 20 / 5 = 4 seconds
after it arrived, since the queue drains at a fixed rate regardless of how the requests arrived.
This is the direct trade-off against token bucket: leaky bucket accepted all 20 (no immediate
rejections) but imposed queuing delay, where token bucket would have accepted the same 20
immediately (assuming burst >= 20) with no delay at all, at the cost of allowing that
instantaneous spike to actually reach the backend.
Trade-offs and pitfalls
- Fixed window is cheap but boundary-exploitable, as shown above; if you need fixed window's
simplicity without the boundary flaw, a sliding-window variant (weighting the previous window's
count by how much of it overlaps the current moment) closes most of the gap at a small added
cost in bookkeeping. - Token bucket allows the burst to actually reach the backend immediately, which is fine if
the backend can absorb a short spike, but risky if the downstream system (a database, a
third-party API you are proxying) cannot; leaky bucket's smoothing is the better fit when the
concern is protecting a fragile downstream dependency rather than just being fair to callers. - Common wrong turn: setting one limit for everything. A single global rate limit with no
per-client tier lets one high-volume legitimate customer's traffic look identical to abuse, and
forces you to choose between a limit generous enough for your biggest customer (too generous
for abuse defense) or strict enough for abuse defense (too strict for your biggest customer).
Layering per-client limits on top of a global ceiling avoids that false choice. - Common wrong turn: no path for a legitimate high-volume customer who needs more than the
default limit. Rate limiting without a documented, supported way to request a higher
per-client quota turns a capacity-planning conversation into a support escalation every time a
customer scales up.
You need a scalable multi-tenant authorization model for APIs that supports hierarchical roles, resource scoping, and delegation. Specify where policy evaluation should occur (gateway vs dedicated policy service), how to store and cache policies efficiently, and how to support tenant-specific custom roles without exploding policy cardinality.
Sample Answer
Put a cheap, coarse allow/deny check at the API gateway using cached, precomputed permission data, and send only the requests that need real policy logic, delegation checks, custom-role composition, attribute conditions, to a dedicated policy service (a Policy Decision Point, or PDP). Keep tenant-specific customization cheap by representing custom roles as compositions of a small, shared set of permission primitives instead of duplicating a full permission list per tenant.
Where policy evaluation should occur
The gateway is the fast path: it holds a short-TTL cached, flattened "effective permissions" record per (tenant, principal) pair and handles the common case, a simple role check, in-process with no network hop. The dedicated PDP is the slow path: it evaluates anything the gateway's cache can't answer confidently, delegation constraints, attribute-based conditions, or a freshly-changed role not yet cached, and the gateway calls it with a bounded timeout and a defined fail-closed (deny) behavior on timeout.
flowchart LR
Client --> GW[API Gateway]
GW -- cache hit: simple role check --> Allow1[Allow / Deny]
GW -- cache miss or complex rule --> PDP[Policy Decision Point]
PDP -- reads --> Store[(Policy Store: roles, compositions, delegations)]
PDP -- decision --> GW
PDP -- flattened result --> Cache[(Short-TTL Permission Cache)]
GW -. reads .-> Cache
This hybrid exists because a pure-gateway design can't express delegation or attributes without bloating every cache entry, and a pure-PDP design, calling it on every request, adds a network round trip to every single API call, unacceptable at scale.
Storing and caching policies efficiently
Canonical roles (for example viewer, editor, billing_admin) are stored once, globally, as a list of permission primitives (resource_type:action). Tenant custom roles are stored as compositions, a pointer list referencing canonical primitives and optionally other custom roles, never a duplicated full permission list per tenant. At evaluation time, or on role change, flatten a principal's effective permissions into one compact record, cache it keyed by (tenant, principal, role-version), short TTL, and invalidate it on a role or policy change event rather than waiting out the TTL.
Hierarchical roles and delegation
Model the role hierarchy as a directed graph, where a role inherits everything a role below it can do, compute effective permissions by walking the graph once and caching the flattened result, not by walking it on every request. Model delegation as a separate, time-bound, explicitly-scoped grant ("user A can act as role R on resource set S until time T"), evaluated at the PDP rather than folded into the role graph itself, so a delegation's expiry doesn't require touching the underlying role definitions.
Permissions can also be scoped to a specific resource instance, not just a resource type, for example a role that applies only to project P17 rather than to "projects" in general. Represent that as an extra attribute on the composition (which instance it applies to) rather than as a brand-new role, so instance-level resource scoping doesn't reopen the cardinality problem the compositions above were built to avoid.
Avoiding tenant-role cardinality explosion
The failure mode to avoid is storing one full permission list per (tenant, custom role) pair: at thousands of tenants, each with a handful of custom roles, that's a policy store growing linearly with tenant count times role count times permission count. Storing compositions instead grows with the number of distinct composition shapes actually in use, not with tenant count, and many tenants reuse the same handful of role archetypes even when they name their roles differently.
Worked example
Say there are 50 canonical permission primitives (orders:read, orders:write, invoices:read, and so on). A naive design storing the full list explicitly for 10,000 tenants, each with 5 custom roles, stores up to 10,000 x 5 x 50 = 2,500,000 permission rows. Storing compositions instead, where each custom role is a short list referencing on average 5 primitives or canonical roles, stores roughly 10,000 x 5 x 5 = 250,000 rows, a 10x reduction, and the number of genuinely distinct composition shapes across tenants is often far smaller still once identical compositions are deduplicated, since tenants tend to reinvent the same handful of role archetypes ("read-only support," "billing") under different names.
Trade-offs and pitfalls
Gateway-only caching is fast but can't express delegation or fine attribute conditions; PDP-only evaluation is fully expressive but adds a round trip to every request, hence the hybrid above. Caching effective permissions improves latency but introduces staleness, always pair caching with event-driven invalidation so a revoked role takes effect immediately rather than waiting out a TTL. Composition-based custom roles are compact but harder to audit at a glance than a flat list ("what can this tenant's role actually do"), so provide a "resolve and show me the flattened permission set" admin tool rather than expecting operators to read the graph by hand.
Propose detection and mitigation strategies for credential stuffing and automated account takeover attempts on login endpoints. Propose telemetry signals (IP velocity, failed-login patterns, device fingerprinting), anomaly detection heuristics, progressive throttling and challenge mechanisms (CAPTCHA, MFA step-up), and techniques to minimize false positives while blocking automated abuse.
Sample Answer
Direct answer
Credential stuffing (attackers replaying stolen username/password pairs from other breaches
against your login endpoint) and automated account takeover are detected primarily through
behavioral telemetry, not a single rule: velocity per IP and per credential, failed-login
patterns, and device fingerprinting feed an anomaly signal that drives progressive friction
(throttling, then a CAPTCHA, then a step-up to multi-factor authentication, MFA) rather than an
all-or-nothing block. The same abuse-defense posture extends past the login form: public API
endpoints that let a client scrape or excessively read data need their own rate and reputation
controls, and a confirmed compromise needs a remediation path (revoking affected credentials,
notifying affected customers), not just a detection alert.
Structured elaboration
Telemetry signals to collect.
- IP velocity and reputation: request rate per IP, and whether the IP is a known VPN,
residential proxy pool, or Tor exit node, since credential-stuffing tooling routes through
large proxy pools specifically to defeat simple per-IP rate limits. - Failed-login patterns: the tell-tale shape of credential stuffing is a low
success-to-attempt ratio spread across many distinct usernames, each tried only once or
twice, as opposed to a brute-force attack hammering one account with many password guesses; a
detector that only watches "failed attempts against one account" misses stuffing entirely. - Device fingerprinting: signals derived from the client (browser/TLS fingerprint, screen
and font characteristics for a web client) that let you notice the same automated client
reappearing under many different credentials or IPs.
Anomaly detection and progressive response. Combine the signals above into a risk score per
login attempt rather than a single hard rule, and respond proportionally:
- Low risk: allow normally.
- Medium risk: add friction, a CAPTCHA challenge or a short delay, cheap enough that it barely
affects a real user but meaningfully slows down high-volume automation. - High risk: require an MFA step-up even if the password was correct, since a correct password
from a breached credential list does not prove the caller is the legitimate account owner. - Confirmed automation: block or throttle hard at the network layer (the IP or proxy pool).
Minimizing false positives. Progressive challenges exist specifically so that a real user
having a bad day (typo'd password, unfamiliar network) experiences mild friction rather than a
hard lockout, while only sustained, clearly automated patterns escalate to a block. Tune
thresholds against a labeled sample of known-legitimate traffic (users on shared corporate
NATs, for example, who will always look like "many login attempts from one IP") before rolling
out a stricter rule broadly, and alert on the block rate itself, since a spike in legitimate
users getting blocked is a signal the thresholds are miscalibrated, not that abuse suddenly
increased.
Beyond the login endpoint: broader public-API abuse defense. The same abuse-defense posture
applies past login, since a public API that is not itself an auth endpoint can still be scraped
or read excessively (a competitor bulk-harvesting a product catalog or pricing data, for
example): rate limiting and quotas per API key or per identity, the same IP-reputation signals
described above applied to read-heavy endpoints, and CAPTCHA gating specifically on suspicious
UI flows (a signup form, a password-reset form) where a CAPTCHA is tolerable friction, rather
than on every API call, where it would break legitimate programmatic clients entirely. When a
compromise or sustained abuse campaign is confirmed, remediation extends beyond detection:
revoke or force-rotate the specific credentials or API keys involved, and notify affected
customers when their accounts or data were plausibly touched, since detection without a
remediation and notification path leaves the actual harm unaddressed even after the technical
attack is stopped.
Worked example
A login endpoint sees, over one hour: 40,000 login attempts from 6,000 distinct source IPs,
targeting 35,000 distinct usernames, with a 0.6% success rate. Contrast that shape with normal
traffic, where the success rate on real login attempts is typically well above 90%, and failures
cluster on a small number of accounts (someone mistyping their own password) rather than
spreading almost one-to-one across usernames. The "many usernames, each tried once or twice,
overall success rate far below normal" shape is the credential-stuffing signature; a system
tracking only "failed attempts per account" would see nothing unusual, since almost no single
account crosses a per-account threshold. A velocity-plus-fingerprint detector, by contrast, would
flag the aggregate pattern across usernames and IPs, trigger progressive CAPTCHA challenges on
the highest-risk slice of that traffic, and, once confirmed, feed the responsible IP ranges into
a longer-lived reputation block while triggering forced password resets for the small number of
accounts where a stuffing attempt actually succeeded.
Trade-offs and pitfalls
- Per-account thresholds alone miss credential stuffing by design, since the attack's whole
point is spreading load thin across many accounts specifically to stay under any single
account's threshold; detection has to look at the aggregate pattern across accounts, not just
within one. - CAPTCHA fatigue is a real cost. Applying CAPTCHA broadly, including to every API call
rather than only to suspicious UI flows, degrades the experience for legitimate users and
breaks legitimate automated integrations; reserve it for the specific flows and risk tiers
where it is proportionate. - IP reputation is a leaky signal on its own. Shared NATs, corporate networks, and mobile
carrier-grade NAT mean many legitimate users can share one IP; combine IP reputation with
device and behavioral signals rather than blocking on IP alone. - Common wrong turn: treating detection as the finish line. A confirmed stuffing success
(an attacker actually got into an account) still requires the remediation steps: forced
credential rotation for affected accounts, session invalidation, and customer notification
where applicable; a detector that only logs an alert leaves the compromised accounts open.
Unlock Full Question Bank
Get access to all 27 API Security, Authentication and Authorization interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.