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
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.
Make it yours · 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.
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 repoExample 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
No tokena script
Dealer userrole: Dealer
Credit officerrole: CreditOfficer
approve 42
user: ?
Authenticationwho are you?
Authorizationmay you? CanApproveLoan
Approve(42)the action
1/8Three callers want to approve application 42: no token, a dealer user, a credit officer.No token: authentication finds no one and rejects nothing. The user stays anonymous.The endpoint needs a signed-in user. Authorization answers 401 Unauthorized: blocked.Dealer user: the token is valid. Authentication builds the user: dana, role Dealer.Policy CanApproveLoan needs role CreditOfficer. Not this user: 403 Forbidden, rejected.Credit officer: authentication builds the user: role CreditOfficer, approval_limit 100000.The policy's two checks pass: the role, and an approval_limit claim is present (not its size).The action runs. 200 OK: approved by someone allowed to.
In plain words
Authentication: proving who you are. The caller shows a credential: a password on a login page, a cookie in a browser, or a token. The answer is a user, or "anonymous" if there's no credential.
Token: a signed piece of text the app sends with every request, in a header (a labelled line at the top of a request) called Authorization: Bearer <token>. The header name is confusing: it carries the proof of who you are. "Bearer" means whoever holds it is treated as that user. At Navistar the token comes from . How a token is made and checked: .
Claim: one fact about the user inside the token, a name and a value: name = dana, role = CreditOfficer, approval_limit = 100000.
User: the object ASP.NET Core builds from the claims (its type is ). A controller is a class whose methods, called actions, answer requests (); inside it, the user is .
Authorization: deciding what this user may do here. It runs after authentication, because it needs to know who the user is.
401 vs 403:401 Unauthorized means "I don't know who you are" (no token, or a bad one): sign in. 403 Forbidden means "I know who you are, and you may not do this". The names are confusing: 401 is really "unauthenticated". The 401 reply also carries a header, WWW-Authenticate: Bearer, telling the caller to send a bearer token.
[Authorize]: an attribute (a label in square brackets above a class or method) that says "this needs a signed-in user". [AllowAnonymous] says the opposite, and wins if both are there.
Role-based authorization:[Authorize(Roles = "CreditOfficer")]. A role is just a claim of type role. Quick, but each rule is written on each action.
Policy-based authorization: a policy is a named rule registered once in Program.cs, made of requirements that must all pass: a role, a claim, or your own check. Actions say only [Authorize(Policy = "CanApproveLoan")], so changing the rule means changing one place.
Custom requirement and handler: for a check the built-ins can't do ("the approval limit is at least 250,000"), you write a requirement class (what to check) and a handler class (the code that checks it). The live task builds one.
Order: must come before in Program.cs, because the second reads the user the first builds (,).
Resource-based authorization: a check that needs the data itself ("is this application from your dealership?"). Attributes run before the action loads the data, so the action asks the authorization service itself, after loading it. The incident in Deep is what happens without it.
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.
Callertoken
Sign inJWT
Policyrules
Checkseach
ActionApprove
Authorization: Bearer eyJ…
1/6Read. Authentication takes the token from the header.Check and build. Signature, issuer, expiry are fine. The claims become .Collect the rules: 3 in all: signed in ([Authorize]), the role, the claim.Run the handlers. Each requirement goes to its handler, built-in ones like RequireRole too.Decide. Every requirement met, so the request reaches the action.If one failed: anonymous user → challenge, 401. Known user → forbid, 403.
The story
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.
Dealer usersigned in
Credit APIone question
Databaseapplications
approve application 42
signed in? yes
1/4Before: the API checks one thing. Is this caller signed in?A dealer user is signed in too, so the approval goes through.Fix: two checks. First who are you, then may you do this?The dealer gets 403 Forbidden: signed in, but not allowed. Only a credit officer gets through.
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.
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:
Tokensent with the request
1/4The request carries a token. Inside are claims: facts about the user.Authentication checks the token is real, then copies each claim onto a user.A role is just a claim of type role. Here: CreditOfficer.Authorization later reads these claims to decide: may this user approve?
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.
What is the difference between 401 and 403?
401 Unauthorized means the API doesn't know who you are: no token, an expired one or a forged one. The response carries a WWW-Authenticate header that says how to sign in. 403 Forbidden means it knows exactly who you are and you're not allowed. In ASP.NET Core the authorization middleware decides: anonymous user and a rule that needs one → challenge (401); known user who fails the rule → forbid (403).
ReportedRole-based, claims-based or policy-based: which do you use?
Roles are the simplest: [Authorize(Roles = "CreditOfficer")]. Claims-based checks any fact in the token, like RequireClaim("approval_limit"). Policies wrap either, plus custom requirements, under one name registered in Program.cs. I use policies for anything beyond "signed in", because the rule lives in one place and the controllers only name it. Roles and claims are really both inside policies anyway: [Authorize(Roles = ...)] builds a small policy behind the scenes.
How do you make sure a user can only see their own records?
Load the record first, then call (the policy is written in the incident). Its handler compares the dealer id claim on the token with the dealer on the record. Attributes can't do this, because they run before the record is loaded. Return 404 rather than 403 for someone else's record, so the API doesn't confirm it exists. The incident in Deep is this exact hole. The full picture: .
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
What is the difference between 401 and 403?
401 Unauthorized means the API doesn't know who you are: no token, an expired one or a forged one. The response carries a WWW-Authenticate header that says how to sign in. 403 Forbidden means it knows exactly who you are and you're not allowed. In ASP.NET Core the authorization middleware decides: anonymous user and a rule that needs one → challenge (401); known user who fails the rule → forbid (403).
ReportedRole-based, claims-based or policy-based: which do you use?
Roles are the simplest: [Authorize(Roles = "CreditOfficer")]. Claims-based checks any fact in the token, like RequireClaim("approval_limit"). Policies wrap either, plus custom requirements, under one name registered in Program.cs. I use policies for anything beyond "signed in", because the rule lives in one place and the controllers only name it. Roles and claims are really both inside policies anyway: [Authorize(Roles = ...)] builds a small policy behind the scenes.
How do you make sure a user can only see their own records?
Load the record first, then call (the policy is written in the incident). Its handler compares the dealer id claim on the token with the dealer on the record. Attributes can't do this, because they run before the record is loaded. Return 404 rather than 403 for someone else's record, so the API doesn't confirm it exists. The incident in Deep is this exact hole. The full picture: .
What does Roles = "A,B" mean, and two [Authorize] attributes?
A comma list means either role is enough. Two attributes on the same action must both pass, so [Authorize(Roles = "CreditOfficer")] plus [Authorize(Roles = "CreditManager")] needs a user who has both.
// either role
[Authorize(Roles =
"CreditOfficer,CreditManager")]
// both roles
[Authorize(Roles = "CreditOfficer")]
[Authorize(Roles = "CreditManager")]
How does the API know the user's roles?
They're claims in the token. At Navistar they are app roles: an admin assigns a person to in Entra, and the next token they get carries it. The API doesn't look them up in a database on every call. The cost: a removed role stays in tokens already issued until they expire, which is why access tokens are short-lived (). What a claim is: .
ReportedHow do you authenticate an MVC site compared with a Web API?
An MVC site (a website where the server builds the pages) that renders pages usually signs in with a cookie: the user logs in once, the server sets an encrypted cookie, and the browser sends it with every page. A failed check redirects to the login page. A Web API called by an Angular app or another service uses bearer tokens instead and answers 401 or 403, never a redirect. Authorization ([Authorize], roles, policies) works the same way on both.
What happens if [AllowAnonymous] and [Authorize] are both on an action?
[AllowAnonymous] wins: the authorization middleware skips every rule on that endpoint. That's why it belongs only on things that really must be open, like the health probe or the login endpoint, and why a review checks for it.
How would you test authorization?
Integration tests (they start the whole API, here in memory with , and call it like a client). A test sign-in handler stands in for , so each test picks the user's claims. For every protected endpoint: no user → 401, wrong role → 403, right role → 200, and for by-id endpoints, another dealer's record → 404. One table-driven test (a single test that loops over a list of endpoints) covers all of them, so a new one can't be forgotten.
ReportedWhich security checks are there besides authentication and authorization?
HTTPS everywhere, input validation, rate limiting (a cap on calls per caller), CORS (the browser rule for which websites may call the API; ), secrets in instead of config files, parameterised queries (values sent apart from the SQL text, so input can't change the query: SQL injection), and logs that never contain tokens or personal data. Securing an API end to end is its own card. CORS on its own page: ; the card: .
Each one: what you see, why, the fix in code, and the line to say.
Every credit officer got 403 after a token clean-up
HTTP 403 Forbidden on every approval
Authorization failed. These requirements
were not met:
RolesAuthorizationRequirement:User.IsInRole
must be true for one of the following
roles: (CreditOfficer)
Cause
puts a user's app roles in a claim called roles. By default the JWT handler renames incoming claims to long .NET names, and roles becomes the .NET role claim, so role checks work. A set MapInboundClaims = false to keep the short names in the logs. Now the claim stayed roles, but role checks still looked for the long .NET role name. found nothing, and every officer got 403.
Fix
Tell the handler which claim holds roles: . A test now calls the approve endpoint with a real-shaped Entra token and expects 200.
Diagram · step 1Before: the token has roles = CreditOfficer. Mapping is turned off, so the name stays roles.
Tokenfrom Entra ID
IdentityJWT handler
PolicyRequireRole
roles: CreditOfficer
claim kept as roles
Before · Program.cs · line 4 · 1 of 2With mapping off, roles stays roles, and nothing yet tells .NET that roles holds the roles.
Say it
We turned off claim mapping and role checks silently stopped matching, because roles now sat in a claim the framework wasn't looking at. Setting the role claim type fixed it, and a test with an Entra-shaped token guards it.
Approve crashed with a 500 after a policy was renamed
System.InvalidOperationException: The
AuthorizationPolicy named: 'CanAproveLoan'
was not found.
Cause
Policy names are plain text. One action said CanAproveLoan (one p) while Program.cs registered . It compiled; the first call to that action threw, and the caller got 500, not 403.
Fix
Keep policy names in one class of constants and use them in both places, so a typo fails the build. A startup test requests every endpoint once.
Before · ApplicationsController.cs · line 1 · 1 of 1Typo: no policy with this name exists.
Say it
Policy names are strings, so a typo compiles and only fails at run time. We keep them as constants in one class, so the compiler catches it.
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
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.
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.
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.
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.
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.
Dealer usersUDC dealerships
Dealer portalAngular, in the browser
Dealer portal APIASP.NET Core
Azure SQLloan applications
You + tech leadL3: our code
App Insightsrequest logs, KQL
Azure DevOpsPR, tests, release
Staging slotthe next build waits
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
dotnet new webapi -n BolaLab, add JWT bearer authentication, and an endpoint GET /applications/{id} with [Authorize(Roles = "Dealer")] returning a record with a .
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.)
Call the endpoint for D-104's record with the D-221 token: 200.
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."
SourcesSourced: 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
Don't say401 means you're not allowed.Say401 means not authenticated: no valid token. 403 means authenticated but not allowed.
Don't sayThe authentication middleware rejects requests without a token.SayIt only builds the user; with no token the user is anonymous. Authorization rejects, and only on endpoints that need a user.
Don't sayCheck the role with an if inside each action.SayPut the rule in a named policy in Program.cs and reference it with [Authorize(Policy = ...)], so it's in one place and tested once.
Don't sayThe Dealer role protects each dealer's records.SayA role says what kind of user it is, not which records are theirs. Load the record and use resource-based authorization.
Traps
What should you say instead? Tap to check.
Don't say401 means you're not allowed.
Say instead
401 means not authenticated: no valid token. 403 means authenticated but not allowed.
Don't sayThe authentication middleware rejects requests without a token.
Say instead
It only builds the user; with no token the user is anonymous. Authorization rejects, and only on endpoints that need a user.
Don't sayCheck the role with an if inside each action.
Say instead
Put the rule in a named policy in Program.cs and reference it with [Authorize(Policy = ...)], so it's in one place and tested once.
Don't sayThe Dealer role protects each dealer's records.
Say instead
A role says what kind of user it is, not which records are theirs. Load the record and use resource-based authorization.
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.
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.
Which base class gives the handler the requirement already typed?
What must the handler call when the check passes, and what if it fails?
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.
Authorizationmiddleware
PolicyCanApproveLoan
HandlerApprovalLimit
check user
role ✓ claim ✓
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.