APIs are the part of a system an attacker doesn't need to guess at — the contract is often public, versioned, and documented in an OpenAPI file. That's exactly why API security interviews at the architect level go past "do you use HTTPS" into authorization logic, abuse economics, and trust boundaries between services. Interviewers are checking whether you think about an API the way an attacker does: as a set of object references, business flows, and trust assumptions to probe, not just a list of endpoints to secure with [Authorize]. This page works through the OWASP API Security Top 10, rate limiting, input validation, CORS, mutual TLS, and the API-keys-versus-tokens decision that comes up in almost every architecture review.

Q1 Walk through the OWASP API Security Top 10. Which risks does an ASP.NET Core framework handle for you, and which ones are entirely on you?#

Short answer: The 2023 list is Broken Object Level Authorization, Broken Authentication, Broken Object Property Level Authorization, Unrestricted Resource Consumption, Broken Function Level Authorization, Unrestricted Access to Sensitive Business Flows, Server-Side Request Forgery, Security Misconfiguration, Improper Inventory Management, and Unsafe Consumption of APIs — and the uncomfortable pattern is that ASP.NET Core's authentication and model-binding pipeline handles almost none of the top three by default, because they're business-logic decisions, not framework failures.

Authentication middleware ([Authorize], JWT bearer validation, AddAuthentication) reliably answers "who is this caller" — that part the framework does well. What it can't do is answer "should this authenticated caller see this specific object," which is exactly API1 (BOLA) and API5 (broken function-level authorization); you have to write that check yourself, every time an ID comes from the client. API3 (broken object property level authorization, which folded the old "excessive data exposure" and "mass assignment" categories into one) is a DTO design problem, not a framework problem. Where the framework genuinely helps: Microsoft.AspNetCore.RateLimiting gives you API4 mitigation out of the box, the CORS middleware constrains API8-style misconfiguration if you configure it deliberately rather than with AllowAnyOrigin, and OpenAPI generation (Microsoft.AspNetCore.OpenApi or Swashbuckle) directly addresses API9 by keeping your endpoint inventory honest — as long as you actually keep it wired to the real routes instead of a stale hand-written spec.

What interviewers look for: that you can map each risk to whose responsibility it is — framework, library, or your business logic — rather than reciting the list as ten equally-weighted bullet points.

Common mistakes: assuming [Authorize] alone closes BOLA; treating the Top 10 as a checklist to run once instead of a set of risks that resurface with every new endpoint.

Q2 Explain BOLA/IDOR with a concrete ASP.NET Core example. Why does routing make this bug easy to introduce?#

Short answer: Broken Object Level Authorization happens when an endpoint trusts an object identifier from the request (a route parameter, a query string, a body field) without verifying the authenticated caller actually owns or is entitled to that specific object — and it's easy to introduce in ASP.NET Core precisely because route binding makes retrieving-by-ID effortless, with no ownership check anywhere in that path by default.

C#
// Vulnerable: any authenticated user can read any invoice by guessing/incrementing the id.
app.MapGet("/invoices/{id:int}", async (int id, AppDbContext db) =>
    await db.Invoices.FindAsync(id) is { } invoice ? Results.Ok(invoice) : Results.NotFound())
    .RequireAuthorization();

// Fixed: the query itself is scoped to the caller, not just gated by authentication.
app.MapGet("/invoices/{id:int}", async (int id, AppDbContext db, ClaimsPrincipal user) =>
{
    var customerId = user.GetCustomerId();
    var invoice = await db.Invoices.FirstOrDefaultAsync(i => i.Id == id && i.CustomerId == customerId);
    return invoice is not null ? Results.Ok(invoice) : Results.NotFound();
}).RequireAuthorization();

The vulnerable version compiles, passes every unit test that logs in as a valid user, and even passes a casual security review that just checks "is this endpoint behind [Authorize]" — which is exactly why it's the single most common finding in real API penetration tests. The fix isn't more authentication, it's authorization scoped to the resource: every query that resolves an ID from client input needs an ownership or entitlement predicate baked in, ideally enforced consistently through a repository layer or resource-based authorization handler so individual endpoints can't opt out by accident. Using non-guessable identifiers (GUIDs instead of sequential integers) raises the cost of blind enumeration but is not a substitute for the ownership check — a GUID leaked in one response, a log line, or a referrer header is just as exploitable as a guessed integer.

What interviewers look for: recognizing that BOLA is an authorization gap, not an authentication gap, and that it survives normal testing because the "happy path" test always uses a user who legitimately owns the resource.

Follow-up questions:

  • How would you enforce this consistently across fifty endpoints without repeating the same predicate everywhere?
  • Why doesn't switching from sequential IDs to GUIDs fix the underlying vulnerability?

Q3 What does "broken authentication" mean for an API specifically, and what mistakes actually cause it in ASP.NET Core services?#

Short answer: For APIs, broken authentication is less about "no password policy" and more about token handling: weak or missing signature validation, tokens that never expire, credentials passed in URLs where they end up in logs and browser history, and inconsistent authentication across an API surface that has grown multiple entry points over time.

The classic ASP.NET Core mistake is a JWT bearer configuration that's too permissive by default — TokenValidationParameters with ValidateIssuer or ValidateAudience left false "to make the demo work," which then ships to production and lets a token minted for a completely different application be accepted. Another common one: services that validate a JWT's signature but never check exp, or that treat a valid signature as sufficient without checking the token hasn't been revoked (which matters for high-privilege actions, since JWTs are stateless by design and can't be revoked without an explicit deny-list or short expiry plus refresh). API gateways make this worse when they authenticate at the edge but internal services trust an internal header (X-User-Id) without also verifying it came through the gateway and not directly from a caller who reached the internal network — effectively broken authentication hiding behind a network boundary that isn't actually enforced. The fix is consistency: one authentication scheme, fully validated (issuer, audience, lifetime, signing key), applied uniformly, with short-lived access tokens and a deliberate revocation strategy for anything sensitive.

What interviewers look for: naming token-specific failure modes (validation parameters, revocation, internal trust boundaries) instead of restating general password-hygiene advice that doesn't map to how APIs actually authenticate.

Common mistakes: trusting an internal header for identity without verifying the request actually came through the authenticating gateway; disabling token validation checks temporarily and forgetting to re-enable them.

Q4 How would you design rate limiting for a public ASP.NET Core API? Walk through the built-in middleware's algorithms.#

Short answer: Microsoft.AspNetCore.RateLimiting gives you four limiter algorithms — fixed window, sliding window, token bucket, and concurrency — registered through AddRateLimiter and applied globally or per-endpoint; the right design usually layers a cheap IP- or API-key-based limiter at the edge with a tighter, identity-aware limiter on specific expensive or abuse-prone endpoints.

C#
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.User.Identity?.Name ?? httpContext.Connection.RemoteIpAddress?.ToString() ?? "anon",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1),
                QueueLimit = 0
            }));

    options.AddTokenBucketLimiter("bursty-search", opt =>
    {
        opt.TokenLimit = 20;
        opt.TokensPerPeriod = 5;
        opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
    });
});

Fixed window is the cheapest and simplest but allows a burst right at the window boundary (twice the limit across two adjacent windows); sliding window smooths that out at slightly more bookkeeping cost. Token bucket is the right fit for "allow bursts, but cap sustained rate" traffic patterns like search or autocomplete. Concurrency limiting caps simultaneous in-flight requests rather than requests per time period, which matters for endpoints backed by a slow downstream dependency where the real risk is exhausting connection pools, not request volume. RateLimitPartition is what makes this practical at scale — partitioning by user identity, API key, or IP means one abusive caller gets throttled without punishing every other tenant sharing the service, which a single global limiter cannot do.

What interviewers look for: matching the algorithm to the abuse pattern instead of defaulting to "just add a fixed window everywhere," and partitioning by caller identity rather than applying one limit to the whole service.

Common mistakes: rate limiting only at the API gateway and assuming individual services never need their own limits; forgetting that UseRateLimiter must run after UseRouting when endpoint-specific policies via [EnableRateLimiting] are in play.

Q5 What's the difference between rate limiting and protecting against "unrestricted access to sensitive business flows" (API6:2023)? Give an example.#

Short answer: Rate limiting caps raw request volume from a given caller; API6 is about protecting a legitimate business flow — buying a ticket, redeeming a coupon, creating an account — from being abused at a volume or pattern that's individually indistinguishable from normal traffic but collectively harmful, which a simple per-IP or per-key request cap often can't detect at all.

The canonical example is ticket-scalping bots: each bot makes requests at a rate well under any reasonable rate limit, but distributes the load across thousands of residential IPs and freshly-created accounts, so no single caller ever trips a threshold while the aggregate effect buys out an entire on-sale in seconds. The same shape shows up in gift-card and coupon-code cracking (low-and-slow enumeration across many accounts), fake account creation for downstream abuse, and scraping a pricing or inventory API at a rate that looks like normal browsing per-session but adds up to a competitor rebuilding your catalog. Because these attacks are defined by pattern and intent, not raw volume, the defenses live above simple rate limiting: device fingerprinting and behavioral signals to distinguish bot traffic from human traffic, step-up friction (CAPTCHA, email/SMS verification) triggered specifically on the sensitive flow rather than the whole API, anomaly detection on aggregate metrics (accounts created per minute from a subnet, redemption attempts per coupon code) rather than per-caller counters, and treating the sensitive flow's identity model itself as part of the defense — for example, requiring a verified account with payment history before allowing ticket purchases, not just requiring "any authenticated user."

What interviewers look for: recognizing that this risk exists specifically because individually-compliant, aggregately-harmful traffic defeats caller-level rate limiting, and naming business-logic-aware controls rather than "just lower the rate limit."

Common mistakes: assuming API4's rate limiting already covers this risk; only ever discussing this in terms of e-commerce, when it applies equally to any business flow with scarce or valuable outcomes (loan applications, referral bonuses, account creation).

Q6 Explain mass assignment. Show a vulnerable endpoint and how you'd fix it.#

Short answer: Mass assignment happens when a request body is bound directly onto a domain or persistence model that has more properties than the client should be allowed to set, so a caller can set fields — IsAdmin, AccountBalance, Role — that were never meant to be client-writable, simply by adding them to the JSON payload.

C#
// Vulnerable: binding straight to the entity lets the client set IsAdmin.
app.MapPost("/users/{id}/profile", async (int id, User update, AppDbContext db) =>
{
    db.Entry(update).State = EntityState.Modified; // client-controlled entity, including IsAdmin
    await db.SaveChangesAsync();
    return Results.Ok();
});

// Fixed: a request DTO exposes only the fields a client is allowed to change.
public record UpdateProfileRequest(string DisplayName, string Bio);

app.MapPost("/users/{id}/profile", async (int id, UpdateProfileRequest req, AppDbContext db) =>
{
    var user = await db.Users.FindAsync(id);
    if (user is null) return Results.NotFound();
    user.DisplayName = req.DisplayName;
    user.Bio = req.Bio;
    await db.SaveChangesAsync();
    return Results.Ok();
});

The DTO pattern is the reliable fix because it makes "what the client can set" an explicit, reviewable contract instead of an implicit consequence of whatever properties happen to exist on the entity today — the entity can grow an IsAdmin field six months later and the request DTO simply doesn't expose it, with no new vulnerability introduced. This is also why API3:2023 merges mass assignment with excessive data exposure: they're the same root cause pointed in opposite directions — one lets the client write fields it shouldn't, the other lets the response leak fields it shouldn't — and both are fixed by the same discipline of explicit, narrow DTOs on both sides of the wire rather than serializing domain entities directly.

What interviewers look for: recognizing that this is a modeling discipline (explicit DTOs), not a library feature you toggle on, and that the same discipline covers both the request and response side.

Common mistakes: binding request bodies directly to EF Core entities "to save time"; adding an allow-list of excluded properties instead of an explicit include-list, which silently reopens the hole every time a new field is added.

Q7 How does CORS actually protect users, and what's a misconfiguration that quietly defeats it?#

Short answer: CORS is a browser-enforced rule, not a server-side security boundary — the server declares which origins may read its responses via headers, and the browser is what refuses to expose the response to JavaScript on a disallowed origin; a server that reflects any Origin header back and also allows credentials defeats the entire model, because it tells every browser that every origin is trusted with the user's cookies.

ASP.NET Core's CORS middleware actually blocks the most dangerous combination outright: configuring both AllowAnyOrigin() and AllowCredentials() on the same policy produces an invalid CORS response, because the CORS specification itself forbids Access-Control-Allow-Origin: * alongside credentials. The misconfiguration that still gets through is the manual workaround some teams reach for instead — SetIsOriginAllowed(_ => true) combined with AllowCredentials(), which dynamically reflects whatever Origin header the browser sent as if it were an explicit allow-list entry, achieving the same "any site, with the user's cookies" outcome the framework was trying to prevent, just without tripping the built-in guardrail. The correct pattern is a real allow-list — WithOrigins("https://app.contoso.com"), or SetIsOriginAllowedToAllowWildcardSubdomains for a known set of subdomains — never a function that says yes to everything. It's also worth remembering CORS protects browsers specifically: it does nothing against a server-to-server call, a mobile app, or curl, none of which enforce same-origin policy, so CORS is never a substitute for actual authentication and authorization on the API itself.

What interviewers look for: the "CORS is enforced by the browser, not the server" framing, and recognizing SetIsOriginAllowed(_ => true) plus credentials as functionally the same hole AllowAnyOrigin plus credentials is, just less obviously.

Common mistakes: believing CORS "secures the API"; leaving a wildcard or reflect-all policy in place after using it to unblock local development.

Q8 When would you use mutual TLS for API-to-API authentication instead of OAuth client credentials?#

Short answer: mTLS proves the identity of the connection — both sides present and validate X.509 certificates during the TLS handshake itself, before any application data flows — while OAuth client credentials proves identity at the application layer via a bearer token; they solve overlapping but distinct problems, and many high-assurance architectures use both together.

mTLS is the stronger fit for a small, relatively static set of trusted services — internal service-to-service calls inside a mesh, or partner integrations with a handful of known counterparties — because certificate issuance and rotation has real operational overhead that doesn't scale gracefully to thousands of dynamically-provisioned clients. OAuth client credentials fits the opposite shape well: many clients, frequent provisioning and de-provisioning, centralized token issuance and revocation, and the ability to encode fine-grained scopes into the token itself rather than just "this is a trusted certificate." In ASP.NET Core, mTLS is configured at the Kestrel level (ClientCertificateMode on the HTTPS endpoint options) plus AddCertificate()/CertificateAuthenticationOptions to turn a validated client certificate into a ClaimsPrincipal; note that certificate authentication failures return 403 Forbidden rather than 401 Unauthorized, because by the time the authentication handler runs, the TLS connection has already been established or rejected — there's no way to "challenge" for a certificate after the fact the way you can challenge for a bearer token. The combination — mTLS for the network-level connection identity, a scoped OAuth token on top for application-level authorization — is standard in zero-trust service-mesh designs, where the certificate proves "this is a legitimate node," and the token proves "this call is allowed to do X."

What interviewers look for: understanding mTLS and OAuth as complementary layers (transport identity vs. application authorization) rather than competing alternatives to pick one of.

Follow-up questions:

  • Why can't a client certificate failure produce a 401 the way an expired bearer token can?
  • How would you rotate client certificates across dozens of partner integrations without a coordinated outage?

Q9 API keys vs. bearer tokens — when is each appropriate, and what are the failure modes of each?#

Short answer: An API key is a long-lived, opaque, usually unscoped credential that identifies which client or project is calling — good for metering, quota, and coarse-grained access to a public API. A bearer token (JWT or opaque OAuth token) identifies which user, with which scopes, until when — the right choice whenever you need per-user authorization, short lifetimes, or fine-grained permissions rather than just "which app is this."

API keys fail in predictable ways: because they're long-lived and typically don't expire on their own, a leaked key (committed to a public repo, embedded in a mobile app's binary, logged accidentally) stays valid until someone notices and manually revokes it — there's no built-in expiry to bound the blast radius. They also don't carry identity beyond "which client," so an API-key-only design can't express "this specific user can only see their own orders" without layering another authorization mechanism on top. Bearer tokens address both gaps — short expiry limits how long a leaked token is useful, and claims in the token (or scopes on the OAuth grant) carry per-user, per-permission context all the way to the resource server — at the cost of more infrastructure (an identity provider or authorization server) and more moving parts to get wrong (validation parameters, key rotation, clock skew). The pattern many production APIs actually use is both: an API key identifies the calling application for rate limiting and billing, and a bearer token, required alongside it, carries the authenticated user's identity and scopes for the actual authorization decision.

What interviewers look for: treating this as "different problems, different tools" rather than "tokens are newer so they're always better" — API keys are still the right choice for simple, per-application quota enforcement.

Common mistakes: using a single static API key as the only credential for a multi-user application, which collapses per-user authorization into "anyone with the key can do anything."

Q10 How would you protect a .NET service against SSRF when it fetches a user-supplied URL, and what does "unsafe consumption of APIs" add on top?#

Short answer: Server-Side Request Forgery happens when your server makes an outbound request to a URL an attacker controls, letting them use your server's network position to reach internal services, cloud metadata endpoints, or systems that shouldn't be reachable from the internet; the defense is validating and constraining the destination before the request goes out, not just checking that the URL "looks like" a URL.

C#
private static readonly string[] AllowedHosts = ["api.trusted-partner.com"];

async Task<HttpResponseMessage> FetchUserSuppliedUrlAsync(Uri url, HttpClient client, CancellationToken ct)
{
    if (url.Scheme != Uri.UriSchemeHttps || !AllowedHosts.Contains(url.Host))
        throw new InvalidOperationException("URL is not on the allowed host list.");

    var addresses = await Dns.GetHostAddressesAsync(url.Host, ct);
    if (addresses.Any(a => IPAddress.IsLoopback(a) || a.IsPrivate()))
        throw new InvalidOperationException("Resolved address is not publicly routable.");

    return await client.GetAsync(url, ct);
}

An allow-list of hosts is the strongest control when the set of legitimate destinations is known; when it isn't, you still need to block requests to loopback, link-local, and private address ranges (including after DNS resolution, since a hostname can resolve to an internal IP even if the hostname itself looks external — DNS rebinding), disable automatic redirect-following or re-validate the destination after every redirect, and never let the outbound call carry the service's own internal credentials or a cloud metadata endpoint's implicit trust. "Unsafe consumption of APIs" (API10:2023) is the mirror image: developers tend to trust data coming back from a third-party API more than user input, skipping the same validation, size limits, and timeout discipline they'd apply to an inbound request — so a compromised or misbehaving upstream partner API can push oversized payloads, malformed data, or exploit a deserialization gap. The fix is symmetrical: treat every response from a third party with the same schema validation, size limits, and timeout/circuit-breaker discipline you'd apply to any untrusted input.

What interviewers look for: knowing that hostname allow-listing alone is insufficient without also checking resolved IPs (the DNS-rebinding angle), and connecting SSRF outbound risk to the newer "unsafe consumption" risk on the inbound side.

Common mistakes: validating only the URL's hostname string and not the IP address it actually resolves to; trusting a partner API's response schema without validation because "it's from a partner we trust."

Quick-Fire Round#

QuestionAnswer
What does API1:2023 in the OWASP API Security Top 10 refer to?Broken Object Level Authorization (BOLA/IDOR).
Which two 2019 risk categories merged into API3:2023?Excessive Data Exposure and Mass Assignment.
What ASP.NET Core type partitions a rate limit by caller (IP, user, API key)?PartitionedRateLimiter.
Why does ASP.NET Core reject AllowAnyOrigin() combined with AllowCredentials()?The CORS spec forbids a wildcard origin alongside credentials; it's an invalid, insecure combination.
What HTTP status does failed certificate authentication return, and why not 401?403 Forbidden — there's no way to "challenge" after the TLS handshake already happened.
What's the core weakness of a long-lived, unscoped API key?No built-in expiry, so a leaked key stays valid until manually revoked.
What must you check beyond the hostname string to fully defend against SSRF?The IP address the hostname actually resolves to (to catch DNS rebinding).
Where must UseRateLimiter run relative to UseRouting for endpoint-specific policies?After UseRouting.
What's the OWASP API risk for trusting third-party API responses too much?API10:2023, Unsafe Consumption of APIs.
What single design habit prevents both mass assignment and excessive data exposure?Explicit, narrow request/response DTOs instead of binding or serializing domain entities directly.

How to Prepare#

  • Be able to map each OWASP API Security Top 10 risk to whether ASP.NET Core, a library, or your own business logic is responsible for closing it.
  • Rehearse a live BOLA example (vulnerable code, then the fix) — it is the single most commonly probed API vulnerability in architect loops.
  • Know the four Microsoft.AspNetCore.RateLimiting algorithms by name and which abuse pattern each one fits.
  • Practice explaining CORS as a browser-enforced rule, with the AllowAnyOrigin + credentials and SetIsOriginAllowed(_ => true) + credentials failure modes ready to describe.
  • Have a clear answer for mTLS vs. OAuth client credentials framed as complementary, not competing.
  • Prepare one SSRF defense story that goes beyond hostname checking to resolved-IP validation.