← Home
.NETWeb APIReported by candidates

How do you secure a Web API in ASP.NET Core (HTTPS, authentication, CORS, input validation, rate limits, secrets, XSS and CSRF)?

14 reports in the question index: Wipro Infrastructure Engineering (4), Infosys (2), Deloitte, Capgemini, Cencora, ZeMoSo, Unlock Technologies, Corporate Infotech, Driven IT Solutions and one unnamed company, in first technical rounds up to October 2026.

Say this first · about 30 seconds
Make it yours · Navistar Financial credit applications API (invented example)
The code, simplified

Invented to show the shape, in C#. Tap Next: the API starts up in Azure and reads the key.

Navistar.sln · where these 2 files sit
  • Bureau/
Diagram · step 1The API starts on . Its settings hold only the vault's address.
Program.cs · line 2 · 1 of 3The vault's address: not a secret, so it sits in plain settings. ! tells the compiler "this is not null".

Fix later in your repo Example written for this card: invented, not from a real codebase.

Say it out loud, then check

Show the 30-second answer
  • I secure an API in layers, so one mistake doesn't open everything. The outer layers are HTTPS with HSTS, CORS limited to our own front-end sites, and token authentication, with authorization on by default through a fallback policy, so a new endpoint is protected even if someone forgets the attribute.
  • Inside the endpoint I check that the record belongs to the caller, because broken object level authorization is the most common hole in APIs.
  • Then abuse and secrets: rate limiting per user, and per IP address for anonymous calls, answering with a 429 and a Retry-After header, and secrets in , read through a . I can go into CSRF and XSS if you'd like.
Check your answer: tick what you said

You said 0 of 8

Then add your example: Navistar Financial credit applications API (invented example)
  • Situation: the API calls a with an API key. The key sat in appsettings.Production.json, in the repository, so anyone with read access to the code could use our account, and every pull of the repo copied it.
  • What I did: moved the key to Azure Key Vault, Azure's locked store for secrets. The has a managed identity: Azure gives the app its own identity, so it signs in to with no password in our config. At start-up, loads the vault's secrets into configuration. The rotated the old key (replaced it with a new one), and secret scanning (a tool that blocks a push containing a key) now stops it happening again.
  • Result: no secret value in the repository or in settings (only the vault's address, which isn't a secret); the code reads config["Bureau:ApiKey"] exactly as before. Rotating the key is a change in the vault, not a release.
  • What I'd say: secrets go in behind a , so there's no bootstrap password, and a leaked repository leaks no keys.

See it

1/11Priya, at dealer D-204, opens loan 7 in the dealer app. Each layer checks one thing.

In plain words

The senior answer · about 2 minutes
Layers, so one mistake isn't fatal
  • I think of it as layers in the pipeline. Forwarded headers first, so we know the real client IP behind , then the exception handler, then HTTPS redirection and HSTS, then CORS for our own front ends, then authentication, then rate limiting, then authorization, which is on by default through a fallback policy. Inside the endpoint, validation and a check that the record is the caller's. Each layer stops a different attack, and none of them has to be perfect on its own.
The holes I actually see
  • The biggest one in APIs is broken object level authorization: a valid user changes an id and reads someone else's data. Role checks don't stop it; only a per-record check does, and we test every by-id endpoint with another dealer's id. Next come endpoints that forgot [Authorize], which the fallback policy covers, and over-posting, which DTOs prevent.
Abuse and secrets
  • Public endpoints get rate limits per IP and signed-in ones per user. One detail people miss: the built-in limiter answers 503 unless you set 429, and a 503 tells clients and monitoring the server is down. At BMW FS that paged at 3 am and made a dealer system retry harder: the incident in Deep. Secrets live in , read through a , so there's no password anywhere in our config.
CSRF, XSS, and what I leave to the edge
  • Our APIs take bearer tokens in the Authorization header, which a browser never adds by itself, so cross-site request forgery doesn't apply; cookie-based APIs need SameSite cookies and antiforgery tokens. XSS is mainly an output-encoding problem in the front end, where Angular escapes by default. In front of the , Azure (Azure's public entry point) has a that blocks known attack patterns and floods before they reach our code. That costs money per rule set and request, so we use it on the public-facing APIs.

Every layer, on the real system

The same request on BMW FS's Azure setup: which part of the system does each job, from the dealer's browser to the database.

1/7Token. The dealer signs in with and the app gets a short-lived token.

The story

  1. Before

    BMW FS's dealer API grew over three years, one endpoint at a time. Each developer added the security they remembered: [Authorize] on most controllers (an attribute, a label in square brackets, that makes an endpoint require sign-in; an endpoint is one address the API answers), a CORS rule copied from an old project (a browser rule about which other websites may call the API), a few checks inside actions. [AllowAnonymous] is the opposite label: open to everyone. Nothing was wrong on purpose; it just depended on everyone remembering everything. Every request passes through a pipeline, an ordered list of steps set up in Program.cs; the fix puts the security there.

    1/4Before: every endpoint carries its own security, added by whoever wrote it.

  2. Why it had to change

    Security that depends on every developer remembering every rule fails the first time someone forgets. In BMW FS's case, a new dealers endpoint shipped without [Authorize], and for two weeks it answered anyone who knew the address.

  3. How it works now

    A senior answer to "how do you secure an API" is defence in depth: several layers, each stopping a different attack, set up once in Program.cs so every request passes them. HTTPS for the wire, CORS for browsers, authentication and authorization for who may call, rate limits for abuse, and inside the endpoint, validation and a check that the record is the caller's. Secrets stay out of the code. The main diagram walks one request through the layers, then three attacks.

  4. What we gained

    New endpoints are protected without anyone remembering to protect them, and each kind of attack has a known layer that stops it. The rest of the card covers secrets, CSRF and XSS, and the night BMW FS's new rate limiter answered with the wrong status code.

In the lending API

The pipeline: each line is one layer of the diagram above, in the same order. Set-up comes first in the file, so the walk jumps back to it. Layer 1, HTTPS redirection, is just above these four.

Only the key lines are shown; Deep walks the whole file.

Lending.sln · where this file sits
  • src/BmwFs.Dealer.Api/
Program.cs · line 75 · 1 of 4Layer 2: CORS headers for our dealer sites, before authentication so preflights (the browser's permission check, sent without a token) aren't refused.

The code, file by file

The BMW FS dealer API's Program.cs: every layer, in pipeline order. Tap Next to walk one request.

Lending.sln · where these 2 files sit
  • src/BmwFs.Dealer.Api/
    • Controllers/
Program.cs · line 5 · 1 of 42Reads the address ranges of from config: Azure's public entry point, a proxy (a server in front of our API that passes calls on). It is the only sender we trust to tell us the client IP.

What they ask next

The follow-ups interviews really ask. Answer out loud, then tap to check.

Follow-up tree

Answer, then open the next level. That is the order an interviewer digs in.

Reporteda candidate was asked Intervieweran interviewer asks it

Production bugs, and how to tell them

Each one: what you see, why, the fix in code, and the line to say.

Production incident: The new rate limiter paged on-call at 3 am and broke a dealer's price sync

P3 · minorBMW FSonly under loadPayment calculator API (ASP.NET Core on App Service, behind Front Door)

Composed, not a real event at BMW FS. The mechanism is real ASP.NET Core, checked by a run on .NET 10: AddRateLimiter rejects with 503 Service Unavailable unless RejectionStatusCode is set, sends no Retry-After header unless OnRejected adds one, and keeps one counter per partition key. Times, counts, names, ticket and PR numbers are composed.

The story in 5 lines
  1. At 03:10 ET on Sat 10 Oct the alert "failed requests above 5%" paged for the payment calculator API, which dealer websites use to show monthly payments.
  2. Every failure was a 503 that took no time and logged no exception: the rate limiter added in Friday's release 6.4 to stop a price-scraping bot. Its rejections used the built-in default status, 503.
  3. A 503 says "the server is down, try again". The alert counted them as outages, and Hudson BMW's dealer system, whose client retries any 5xx three times at once, so it made four times as many calls (1 plus 3 retries), and failed its nightly price sync.
  4. That night a firewall rule blocked the bot and a config change raised the limit for the dealer system. The fix: 429 with a Retry-After header, an alert that counts only 5xx, separate limits for known dealer systems by API key, and the dealer system's vendor now honours Retry-After.
  5. The rule since: a rate limiter answers 429 with Retry-After, and every new security control is load-tested with a real client's traffic before release.
Who's who (5 people)
You
.NET developer on the vendor team (L3). Owns the fix.
Tech lead
Vendor senior. Reviews every PR, joins when paged.
L2 on-call
Vendor support. First to be paged at night.
BMW FS dealer integrations manager
Owns the dealers' system connections to our APIs.
BMW FS IT change manager
Approves emergency and config changes.

Tap a step. The map lights the place where it happens.

1/8 Detect

03:10 ET: failed requests above 5%. The alert pages .

Sat 10 Oct, 03:10 ET, US Eastern (12:40 IST)L2 on-call, paged by the alert

  • What: the alert "failed requests above 5% for 10 minutes" fires for the payment calculator API. Dealer websites call this API anonymously to show a monthly payment next to each car.
  • How we saw it: is paged. Failed requests are at 31%, all status 503 Service Unavailable.
  • Why it looked bad: a 503 normally means the API or something it depends on is down, so the alert treats it as an outage.
The page L2 received
ALERT Sev2 · bmwfs-calculator-api
Failed requests > 5% for 10 min
Current: 31% (resultCode 503)
Fired 03:10 ET
Try it yourself · 10 minutes, on your Mac
  1. dotnet new web -n RateLab. Add with a fixed-window policy of 3 calls a minute, app.UseRateLimiter(), and .RequireRateLimiting(...) on one endpoint.
  2. Call it 4 times with curl: the 4th answer is 503, with no Retry-After.
  3. Set o.RejectionStatusCode = 429 and add the code from the fix. Call it 4 times again: the 4th is 429 with Retry-After.
  4. Partition by a header value instead of the IP, and send two different values: each gets its own 3 calls.
You should see

The limiter's default answer is 503, which means "server down". 429 with Retry-After tells a client it's their rate and how long to wait.

The senior move

"A rejection is part of the API's contract: 429 with Retry-After, and a separate limit for each known partner."

Sources Sourced: ASP.NET Core docs, "Rate limiting middleware" ( default, , , partitioned limiters); HTTP semantics of 429 and Retry-After (RFC 6585, RFC 9110). Checked with a real run on .NET 10: the 4th call over a limit of 3 got 503 without the setting, 429 with it, and Retry-After from ; per-user and per-IP partitions kept separate counts. Composed: BMW FS's systems, the bot, Hudson BMW's , dates, counts, names, ticket and numbers and the .

Traps

Traps

What should you say instead? Tap to check.

Live coding

Easy first, then harder. Set a timer and type it before you look.

Task 1

Question index: Musteasy · 10 minutes

Make every endpoint of an API require a signed-in user by default, except a health check that load balancers call without a token. Add HTTPS redirection and HSTS.

Cencora: how do you maintain security in a Web API?
Stuck? Hints
  1. Which authorization setting applies to endpoints that have no attribute?
  2. How do you mark one endpoint as public?
  3. Why only call outside Development?
Show a solution
Lending.sln · where this file sits
Diagram · step 6Authorization: the fallback policy needs a signed-in user. She is.
Program.cs · line 2 · 1 of 4Endpoints with no attribute need a signed-in user.
What the interviewer checks
  • with RequireAuthenticatedUser() in .
  • The health endpoint marked [AllowAnonymous] (or .AllowAnonymous() on a minimal endpoint).
  • outside Development, , then authentication before authorization.
  • Can say what HSTS adds beyond the redirect.

Task 2

Question index: Mustmedium · 20 minutes

Add rate limiting: 100 calls a minute per signed-in user and per IP for anonymous callers, answering 429 with a Retry-After header.

ZeMoSo Technologies: how do you secure your API?
Stuck? Hints
  1. Which method gives each caller their own counter?
  2. What status does the limiter return if you don't set one?
  3. Where does the limiter know how long the caller must wait?
Show a solution
Lending.sln · where this file sits
Diagram · step 3 reads how long until the window resets and sends 429 with Retry-After.
Program.cs · line 3 · 1 of 8Not the default 503.
What the interviewer checks
  • with , keyed on the user id or the IP.
  • RejectionStatusCode = 429.
  • reads from the lease and sets the Retry-After header.
  • after (so the user is known) and on the endpoints.
With AI tools

Interviewers now ask how AI tools change this. Generated code tends toward the permissive default: , an [AllowAnonymous] to make a test pass, a key pasted into appsettings. I review AI-written Program.cs files against the same layer list as this card, and keep secret scanning on the repository, because assistants happily repeat a key they saw in context. AI is also useful as a reviewer: asked "which endpoints in this controller lack an ownership check?", it finds BOLA candidates fast, which I then verify.

Glossary: every name, one line each →

My notes

Paste the NOTES block from your doubt chat here. The grader never reads notes; the coach reads "Still unclear".

Read moreWhere it was asked