Security Models
By the end of this lesson you'll be able to tell authentication from authorization, and lock down an ASP.NET Core app three ways — by role , by policy , and by claim — choosing the right model for each rule and defaulting to deny so you never leak an endpoint by accident.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Think of getting around a secure office building. A role badge (role-based) opens whole categories of door at once — "Staff", "Manager" — simple, but everyone with that badge gets the same access. A keycard policy (policy-based) is a named rule the reader enforces — "Staff and fire-trained and after 6pm" — a reusable combination of conditions. Personal attributes (claims-based) are the facts printed on the badge itself — your clearance level, your department, your date of birth — and the door decides from those facts, not from your job title. Most real buildings use all three: a badge for the easy cases, policies for the combinations, and the printed facts when a job title is too blunt an instrument.
In ASP.NET Core these aren't rivals — a role is just a claim of type role , and a policy is the general mechanism that role checks and claim checks both run through. Reach for the simplest model that expresses the rule.
1. Authentication vs Authorization
These two words look alike and are constantly confused, but they answer different questions. Authentication (authn) proves who you are — logging in, validating a token. Authorization (authz) decides what you're allowed to do once your identity is known. A user can be perfectly authenticated and still be forbidden: that's a 403 , not a 401 . Authorization always comes second. Read this worked example, run it, then you'll write the role check yourself.
2. Role-Based Authorization (RBAC)
Role-based access control is the simplest model: each user carries a list of role labels, and you grant access when a required label is present. At its core it's just roles.Contains("Admin") . In ASP.NET Core you rarely write that if yourself — you declare it with [Authorize(Roles = "...")] and the framework enforces it. Comma-separated roles mean OR (any one matches); stacking the attribute means AND (all required). First, finish the plain check below.
Your turn. The program below authorizes by role — fill in the two ___ blanks using the hints, then run it.
Here's how the same check looks in a real ASP.NET Core controller. Notice there's no if at all — the [Authorize] attribute is the rule, and the framework returns 403 for you when it isn't met.
3. Claims-Based Authorization
Roles are blunt: "is this person an Admin?" tells you nothing about why . A claim is a single fact about the user — a (type, value) pair like ("age", "21") or ("email_verified", "true") — and claims-based authorization decides access from those facts directly. This is more honest: instead of inventing an "Over18" role, you check the age claim. A policy here is simply a predicate over the user's claims. Read the worked example, then write your own age check.
Now you try. Authorize by a claim rather than a role: read the age fact and apply an 18+ policy. Fill in the two ___ blanks:
4. Policy-Based Authorization in ASP.NET Core
Policy-based authorization is the model the others run through. You register a named policy once with AddAuthorization(o => o.AddPolicy(...)) , combine as many requirements as you like (roles, claims, custom logic), and reuse the name with [Authorize(Policy = "...")] . Inside the app, the signed-in user is a ClaimsPrincipal — you read facts off it with FindFirst , HasClaim , and IsInRole . Crucially, a FallbackPolicy makes the whole app default-deny : every endpoint needs a login unless you explicitly opt out.
Least privilege means every user, token, and service gets the minimum access needed to do its job — and nothing more. A reporting service that only reads data should never hold write credentials; a support agent who resets passwords shouldn't also be able to delete accounts. The blast radius of a stolen token or a compromised account is exactly the privilege it carried.
Two habits make this concrete in C#. First, default-deny : set a FallbackPolicy requiring an authenticated user, then add [AllowAnonymous] only to the genuinely public endpoints. A new endpoint is then locked by default — you can't forget to protect it. Second, authorize by capability, not by person : a policy named "CanRefund" survives reorganisations and new roles, whereas Roles = "SeniorManager" rots the moment the org chart changes.
Q: What's the real difference between authentication and authorization?
Authentication proves who you are (login, token validation) and produces a failed result of 401 Unauthorized . Authorization decides what you may do and fails with 403 Forbidden . You're authenticated first, then authorized.
Use a role for coarse, stable groups ("Admin", "Staff"). Use a claim when the decision depends on a fact about the user — age, department, subscription tier, email-verified. If you're tempted to invent a role like "Over18" , that's really a claim.
Q: Aren't policy-based and claims-based the same thing?
Closely related. A policy is the named container of requirements; a claim requirement is one kind of requirement you can put in it. Claims-based authorization is usually implemented as a policy that requires certain claims.
Q: Why can't I just trust the claims the browser sends?
Because the client controls them. An attacker can set isAdmin=true in a header or an unsigned token. Only authorize on claims your server issued and signed — for example the claims inside a JWT you validated against your signing key.
Default-deny. Set a FallbackPolicy that requires an authenticated user so every endpoint is protected unless you mark it [AllowAnonymous] . That way forgetting to add [Authorize] can't leak a private endpoint.
No blanks this time — just a brief and an outline. Write a tiny authorizer that grants access only when the user has both the "Editor" role and a signed ("email_verified", "true") claim — combining role-based and claims-based checks the way real policies do. Run it and match the expected output.
Practice quiz
What does authentication answer?
- What may you do?
- Where are you?
- Who are you?
- When did you log in?
Answer: Who are you?. Authentication proves WHO you are; authorization decides WHAT you may do.
An authenticated user who is denied access to an admin area gets which status code?
- 403 Forbidden
- 401 Unauthorized
- 404 Not Found
- 200 OK
Answer: 403 Forbidden. A logged-in but disallowed user is 403 Forbidden; 401 means not authenticated.
What does role-based access control (RBAC) decide access on?
- A fact like age
- The request URL
- The time of day
- A role label on the user
Answer: A role label on the user. RBAC grants access when a required role label is present, e.g. roles.Contains("Admin").
In [Authorize(Roles = "Admin,Manager")], what do the comma-separated roles mean?
- The user needs BOTH roles (AND)
- The user needs ANY one of them (OR)
- Neither role is required
- Only the first is checked
Answer: The user needs ANY one of them (OR). Comma-separated roles mean OR - any one matching grants access. Stacking [Authorize] attributes means AND.
How do you require that a user has BOTH the Admin AND SuperAdmin roles?
Stacking [Authorize(Roles="Admin")] and [Authorize(Roles="SuperAdmin")] requires both (AND).
What is a claim?
- A role name
- An HTTP header
- A single fact about a user as a (type, value) pair
- A database table
Answer: A single fact about a user as a (type, value) pair. A claim is a (type, value) fact like ("age", "21") or ("email_verified", "true").
How does ASP.NET Core represent the signed-in user inside the app?
- A string username
- A ClaimsPrincipal
- A DbContext
- An HttpRequest
Answer: A ClaimsPrincipal. The signed-in user is a ClaimsPrincipal; read facts with FindFirst, HasClaim, and IsInRole.
What does a default-deny FallbackPolicy that requires an authenticated user achieve?
- It makes every endpoint public
- It disables authorization
- It speeds up requests
A FallbackPolicy locks new endpoints by default; you opt out per-endpoint with [AllowAnonymous].
Why must you never authorize on a claim the client itself can set?
- It's slower
- The client controls it, so an attacker could set isAdmin=true and elevate themselves
- Claims can't be read on the server
- It breaks JSON serialization
Answer: The client controls it, so an attacker could set isAdmin=true and elevate themselves. Only authorize on claims your server issued and signed (e.g. inside a validated JWT), never client-supplied ones.
When you find yourself inventing a role like 'Over18', what does that usually indicate?
- You need more roles
- Authorization isn't possible
- It's really a claim - model it as a claim, not a role
- You should use 401
Answer: It's really a claim - model it as a claim, not a role. Facts about a user (age, verified email) are claims; reach for the simplest model that expresses the rule.