← Home
.NETWeb APIReported by candidates

What is the difference between authentication and authorization, and how do role-based and policy-based authorization work in ASP.NET Core?

17 reports in the question index (8 linked below): TCS (3), Excellon Software (2), Infosys (2), Capgemini, Grid Dynamics, Tech Mahindra, Synechron, Accenture and others, mostly in the first technical round.

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

Invented to show the shape, in C#. Tap Next: a call before the fix, then after.

Navistar.sln · where these 2 files sit
  • Controllers/
RatesController.cs · line 2 · 1 of 3Gives this controller the address api/rates.

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
  • Authentication is proving who you are, and authorization is deciding what you're allowed to do. In ASP.NET Core the authentication middleware reads the credential, for an API usually a bearer token from or our own login, and turns it into a user with claims: the name, the roles, and other facts like an approval limit.
  • Authorization then checks that user against what the endpoint asks for. If the endpoint needs a signed-in user and there is no one, the answer is 401 Unauthorized, which really means not authenticated. If we know who you are but you're not allowed, it's 403 Forbidden.
  • Role-based is the simple form: an Authorize attribute with a role. Policy-based is what I use for anything real: a named policy registered once that combines a role, a claim, or a custom requirement with its own handler. The controller just names it.
  • Roles can't check the data: a dealer could read another dealer's application by changing the id. So after loading the record, I ask the authorization service whether this user may see it.
Check your answer: tick what you said

You said 0 of 7

Then add your example: Navistar Financial credit API (invented example)
  • Situation: a security test on the Navistar credit API found that , added in a hurry for a pricing screen, had no [Authorize] on it. Anyone who knew the address could read dealer interest rates without signing in. Nothing had leaked, but the next forgotten attribute could leak customer data.
  • What I did: I made the API deny by default. A fallback policy is the rule ASP.NET Core uses for any endpoint that has no [Authorize] and no [AllowAnonymous] of its own. I set it to "needs a signed-in user". Then the only open endpoint, the health probe that Azure calls without a token to check the app is alive, got .AllowAnonymous().
  • Result: the retest passed. A controller someone forgets to mark is now closed: no token gets 401. The health probe still answers 200.
  • What I'd say: I don't rely on people remembering [Authorize]. The fallback policy makes every endpoint need a signed-in user unless it explicitly opts out.

See it

1/8Three callers want to approve application 42: no token, a dealer user, a credit officer.

In plain words

The senior answer · about 2 minutes
Two questions, two middleware
  • Authentication answers who you are. The authentication middleware reads the credential, for our APIs a bearer token from , checks it is genuine, and turns its claims into the user object on . It never rejects a request on its own: no token simply means an anonymous user.
  • Authorization answers what you may do. The authorization middleware looks at the endpoint the router picked, collects its [Authorize] rules, and checks the user against them. If the user is anonymous it sends a challenge (tells the caller to sign in), which is the 401; if the user is known but fails the rule, it forbids (refuses a known user), which is the 403. That's also why the order in Program.cs matters: authorization placed before authentication sees an anonymous user and returns 401 even for a valid token.
Roles, claims and policies
  • A role is just a claim of type role. [Authorize(Roles = "CreditOfficer,CreditManager")] means either role; two separate [Authorize] attributes mean both must pass. It's fine for a small app, but the rule ends up copied on every action.
  • So for anything real I use policies. A policy is registered once in Program.cs and is a list of requirements that must all succeed: a role, a claim, or a custom requirement. A custom requirement is a small class that says what to check, plus an registered in DI (the container that builds objects: ) that does the check, like comparing the officer's approval-limit claim with a threshold. The controller just says [Authorize(Policy = "CanApproveLoan")], so when credit changes the rule, one place changes.
When the rule depends on the data
  • Attributes run before the action, so they can't see the record. For "may this dealer see this application" you load the application and then call with the user, the application and a policy whose handler compares the dealer on the token with the dealer on the record.
  • Skipping that is the most common real hole, called broken object-level authorization: on the UDC dealer portal a dealer opened other dealers' applications by changing the id in the address. That's the incident in Deep. We now return 404 for someone else's record, so the API doesn't even confirm it exists.
What I set up on every API
  • Deny by default: a fallback policy that requires a signed-in user on every endpoint, so a new controller someone forgets to mark is closed, not open, and [AllowAnonymous] only on the health probe.
  • And a test per endpoint that calls it with no token, with the wrong role and with the right one, and checks 401, 403 and 200. Roles come from app roles, so access changes are an Entra change with an approval, not a code change.

Inside one decision

What happens between the request and the action, for the credit officer's approve call, in order.

1/6Read. Authentication takes the token from the header.

The story

  1. Before

    Navistar Financial's credit team approves truck loans in an internal web app. Behind its screens sits our .NET API: the code that does the work when a screen asks. Two of its jobs: any signed-in user may read a loan application, but only a credit officer may approve one. In the first version the API asked one question only: is this person signed in? Every staff member and every dealer user of the portal was.

    1/4Before: the API checks one thing. Is this caller signed in?

  2. Why it had to change

    Being signed in only proves who someone is. It says nothing about what they may do. When the API treats "signed in" as "allowed", any user can do anything, and the only thing stopping them is which buttons the screen happens to show. A dealer who calls the API directly skips the screen.

  3. How it works now

    ASP.NET Core splits this into two steps that run for every request. Authentication answers "who are you?": it reads the caller's token, a signed piece of text the app sends with every request to prove who the user is, and builds a user object from it. Authorization answers "may you do this?": it compares that user with the rule on the endpoint (one address the API answers, like "approve application 42"). A named rule is called a policy. The facts in the token are called claims: one name and one value each. Here is the user that authentication builds from a credit officer's token:

    1/4The request carries a token. Inside are claims: facts about the user.

  4. What we gained

    Each endpoint now states its own rule, and the API enforces it even when someone skips the screen. The rest of the card follows three calls to the same endpoint through both steps.

In the lending API

The controller: two attributes hold the two rules; the diagram above shows them running.

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

Lending.sln · where this file sits
  • src/Navistar.Credit.Api/Controllers/
⋯ 9 lines in Deep⋯ 2 lines in Deep
ApplicationsController.cs · line 3 · 1 of 4Rule for the whole controller: every action needs a signed-in user. No user: authorization answers 401.

The code, file by file

The Navistar credit API: start-up, then the controller. Tap Next to walk the three calls.

Lending.sln · where these 2 files sit
  • src/Navistar.Credit.Api/
    • Controllers/
Program.cs · line 3 · 1 of 11Turns on controllers: the classes that hold your endpoints.

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: A dealer could open any other dealer's loan applications

P1 · outage or data leakUDC Financelatent: hidden for weeksDealer portal API (ASP.NET Core on App Service)

Composed, not a real event at UDC Finance. The mechanism is real ASP.NET Core, checked by a run on .NET 10: a role check alone lets any dealer read any application by id, and a resource-based handler returns 404 for another dealer's record. This hole, broken object-level authorization (BOLA), is first on the OWASP API Security Top 10 (OWASP is a non-profit that lists the main web security risks). Times, counts, names, ticket and PR numbers are composed. OWASP API1:2023 Broken Object Level Authorization

The story in 5 lines
  1. On Tue 13 Oct a finance manager at a UDC dealership typed a different number into the portal's address bar and saw another dealership's customer: name, address, income.
  2. Logs showed it had been possible since the portal's applications page went live in July. Three dealer users had opened applications from other dealerships, one of them 412 in a row.
  3. The API's GET /api/dealer/applications/{id} only checked the role Dealer. Nothing compared the dealer on the token with the dealer on the application.
  4. An emergency the same day returned 404 for other dealers' records. The proper fix: a resource-based authorization handler, and a test for every by-id endpoint.
  5. The rule since: every endpoint that takes an id gets a "another dealer's record → 404" test, and an alert fires when one user reads many applications that aren't theirs.
Who's who (5 people)
You
.NET developer on the vendor team (L3). Owns the fix.
Tech lead
Vendor senior. Runs the bridge call, reviews every PR.
L2 pod
Vendor support. Runs queries and runbooks, deploys with approval.
UDC privacy officer
Decides whether customers and the regulator must be told.
UDC IT change manager
Approves the emergency change.

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

1/8 Detect

A dealer changes the id in the address and sees another dealer's customer.

Tue 13 Oct, 10:20 NZDT (Tue 13 Oct, 02:50 IST)UDC dealer support, then the L2 pod

  • What: a finance manager at an Auckland dealership emails UDC dealer support. Checking one of her own applications, she mistyped the number in the address bar and the portal showed a different dealership's customer.
  • How we saw it: tries it with a test dealer account in production: application ids that belong to another dealer come back 200 with the full record.
  • Why it hid: the portal's screens only ever list your own applications, so no tester typed someone else's id. Every call was a valid, signed-in dealer: no error, no alert.
In theory

An alert when one user reads records that aren't theirs.

In practice

The logs had the user and the application id, but nothing compared them. A dealer found it, not us.

Try it yourself · 15 minutes, on your Mac
  1. dotnet new webapi -n BolaLab, add JWT bearer authentication, and an endpoint GET /applications/{id} with [Authorize(Roles = "Dealer")] returning a record with a .
  2. Create two test tokens with dotnet user-jwts create --role Dealer --claim dealer_id=D-104 (and D-221). (dotnet user-jwts makes a test token for local use.)
  3. Call the endpoint for D-104's record with the D-221 token: 200.
  4. Add the handler and in the action. Call again: 404, and 200 with the right dealer.
You should see

A role check lets every dealer read every record. Only a check after loading the record, comparing it with the signed token, keeps dealers apart.

The senior move

"Roles say what kind of user you are, not which records are yours. Every endpoint that takes an id loads the record, then asks the authorization service whether this user may see it, and returns 404 if not."

Sources Sourced: role-based authorization and resource-based authorization with (ASP.NET Core docs, "Resource-based authorization"); broken object-level authorization is API1 in the OWASP API Security Top 10 2023; the NZ Privacy Act 2020 requires notifying serious privacy breaches. Checked with a real run on .NET 10: a dealer token for D-221 got 200 for D-104's record with the role check alone, and 404 with the handler. Composed: UDC's systems, dates, counts, names, ticket and numbers, the and the URLs.

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

Protect : every action needs a signed-in user, also needs the role , and a /health endpoint stays open with no token.

TCS: how do you authenticate your API calls?
Stuck? Hints
  1. Which attribute on the class covers every action?
  2. How do you add a second rule to one action only?
  3. Which attribute, or method on , opens one endpoint?
Show a solution
Lending.sln · where these 2 files sit
  • Controllers/
ApplicationsController.cs · line 3 · 1 of 2Every action: a signed-in user.
What the interviewer checks
  • [Authorize] on the controller, [Authorize(Roles = "CreditOfficer")] on .
  • before .
  • The health endpoint opted out with .
  • Can say which call gets 401 and which gets 403.

Task 2

Question index: Mustmedium · 20 minutes

Add a policy : the user needs the role and an approval_limit claim, and a custom requirement checks the limit is at least 250,000 (fleet loans). Write the requirement, its handler, and register both.

Grid Dynamics: authentication and authorization in a .NET API
Stuck? Hints
  1. What interface marks a class as a requirement?
  2. Which base class gives the handler the requirement already typed?
  3. What must the handler call when the check passes, and what if it fails?
  4. How does ASP.NET Core find your handler?
Show a solution
Lending.sln · where these 2 files sit
  • Authorization/
Diagram · step 1An officer with approval_limit 100000 calls approve. The middleware runs the policy.
Program.cs · line 3 · 1 of 3The policy: all three requirements must pass.
What the interviewer checks
  • ApprovalLimitRequirement : IAuthorizationRequirement holding the threshold.
  • that reads the claim, parses it safely and calls context.Succeed; on failure it simply doesn't succeed.
  • Handler registered in DI as .
  • Policy combines , and the requirement; all must pass.
  • Knows a limit below 250,000 gives 403, not 401.
With AI tools

Interviewers now ask how AI tools change this. Assistants write [Authorize(Roles = ...)] quickly, but they almost never add a resource check to a by-id endpoint, and when a test fails with 401 they often suggest switching off a check (ValidateIssuer = false, which stops checking who issued the token, or MapInboundClaims = false) to make it pass. I review every generated controller with one question: which records can this user reach through it? And I keep the endpoint table test, so an AI-written endpoint without a rule fails the build.

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