ASP.NET Core 10 APIs · Lesson 10

Development identity and authorization policies

Observe 401 and 403 with an explicitly local teaching scheme.

External .NET 10 SDK 10.0.403; ASP.NET Core server on your own desktop. Kestrel does not run in the SharpRune browser console.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

Authentication and authorization answer different questions

Authentication establishes an identity through a configured scheme. Authorization evaluates whether the resulting principal satisfies an operation's requirements. A missing identity and an insufficient role therefore need different observations. The portable companion evaluates three known states and prints unauthenticated, forbidden and allowed. It uses ordinary booleans and makes no credential or framework claim. The external project uses real ASP.NET authentication and authorization middleware, but its identity source is intentionally an untrusted classroom header. It is suitable only for observing the pipeline on a local Development server and refuses to start in another environment.

Keep the teaching identity source explicitly local

LocalLab accepts exactly reader or editor from X-Lab-User and creates a principal with the corresponding role. Any caller able to send the header can choose editor, so this is not a deployed authentication protocol. The Development startup guard and loopback instructions make that narrow scope explicit. No token signature, credential storage, login session, certificate or JWT verification is implemented or implied. A production application must integrate an appropriate trusted identity provider and authentication handler, then apply its application policies. Copying the header handler into a deployment would not inherit trust from the framework types surrounding it.

Observe challenge, forbid and accepted policy outcomes

The /edit endpoint requires the edit policy, which requires the Editor role. With no accepted header, there is no authenticated principal and the scheme returns a 401 challenge. With reader, the principal exists but lacks Editor, so the response is 403. With editor, authorization succeeds and the endpoint returns allowed true with status 200. The /profile endpoint only requires authentication, so reader can receive its own name there. These requests independently demonstrate the distinction; a successful profile request does not imply access to every endpoint, and a forbidden response does not imply failed authentication.

Build the principal before evaluating its policies

The application explicitly calls authentication middleware before authorization middleware. Policy evaluation then sees the principal established for that request, and endpoint metadata supplies the requirement. Claims and roles are values whose meaning depends on the trusted issuer and scheme in a real system; an arbitrary header has no such trust. Keep application authorization close enough to the operation that a later refactor cannot bypass it accidentally. This lesson has no data ownership checks beyond its fixed role requirement. It does not claim that possessing Editor is sufficient for every future resource or tenant.

Practice: separate unknown identity from insufficient role

Add independent requests with an absent header, an unknown classroom name, reader and editor. Specify expected status for /profile and /edit for each before sending them. Keep the two accepted names unchanged while testing an unknown value, so a fallback identity cannot hide the missing-header boundary. Inspect status rather than an HTML login page or localized message. The hint is that NoResult leaves this teaching request without an authenticated principal, while reader has an authenticated principal that can still fail a particular policy.

Practice: verify the development-only boundary

Run the compiled project with the environment set to Production and verify that it refuses to start before serving any endpoint. Restore Development on loopback and repeat the three baseline policy requests. Then sketch how a real trusted authentication integration would replace LocalLab while preserving the authorization policy tests. Do not implement a homegrown JWT parser or assume that decoding a token establishes its trust. The exercise is to identify the replaceable identity boundary and keep its security responsibilities distinct from the application decision represented by the Editor policy.

Preserve trustworthy meaning when replacing the adapter

The lesson proves how real middleware handles three controlled identity states; it deliberately does not prove a secure credential system. Keep that distinction in test names and documentation. A production integration should verify issuer, audience, credential lifecycle and the chosen scheme's requirements according to its supported documentation, while authorization tests should still cover allowed and denied application actions. The portable companion remains useful for a small decision table, but cannot establish identity trust. The strongest result here is a precise boundary: authenticated principal construction precedes a separately stated policy, and each denial category remains observable.

  • 401 challenges a missing accepted identity; 403 can deny an authenticated principal.
  • The fixed development header is untrusted teaching data, not a deployment credential.
External .NET 10 SDK 10.0.403; ASP.NET Core server on your own desktop. Kestrel does not run in the SharpRune browser console.

Development identity and authorization policies — real ASP.NET Core 10 project

Set up the SDK project

  1. Save all files into one new empty directory, preserving their relative paths. Install the official .NET 10 SDK if it is not already available.
  2. From that directory run: dotnet restore WebLab.csproj --source https://api.nuget.org/v3/index.json
  3. Start only on loopback: ASPNETCORE_ENVIRONMENT=Development dotnet run --project WebLab.csproj --no-launch-profile --urls http://127.0.0.1:5080
  4. Use another terminal for the requests below, then press Ctrl+C in this project’s terminal to stop its server.
  5. Compare curl -i /edit with X-Lab-User: reader and X-Lab-User: editor against this loopback Development server only.

Download complete SDK project ↗

Project files

WebLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

  </PropertyGroup>
</Project>
global.jsonDownload file ↗
{
  "sdk": {
    "version": "10.0.403",
    "rollForward": "latestPatch"
  }
}
Program.csDownload file ↗
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
if (!builder.Environment.IsDevelopment()) throw new InvalidOperationException("LocalLab accepts untrusted classroom headers and is Development-only");
builder.Services.AddAuthentication("LocalLab").AddScheme<AuthenticationSchemeOptions, LocalLabHandler>("LocalLab", _ => { });
builder.Services.AddAuthorization(options => options.AddPolicy("edit", policy => policy.RequireRole("Editor")));
var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization();
app.MapGet("/profile", (ClaimsPrincipal user) => new { name = user.Identity!.Name }).RequireAuthorization();
app.MapGet("/edit", () => TypedResults.Ok(new { allowed = true })).RequireAuthorization("edit"); app.Run();
public sealed class LocalLabHandler : AuthenticationHandler<AuthenticationSchemeOptions> {
    public LocalLabHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) { }
    protected override Task<AuthenticateResult> HandleAuthenticateAsync() {
        string name = Request.Headers["X-Lab-User"].ToString();
        if (name != "reader" && name != "editor") return Task.FromResult(AuthenticateResult.NoResult());
        var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, name), new Claim(ClaimTypes.Role, name == "editor" ? "Editor" : "Reader") }, Scheme.Name);
        return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name)));
    }
}

Expected result

Development-only /edit gives 401 without an accepted X-Lab-User header, 403 for reader, and 200 for editor. /profile with reader is 200. The header is not an authentication mechanism for deployment.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

foreach (var state in new[] { (false, false), (true, false), (true, true) }) Console.WriteLine(!state.Item1 ? "unauthenticated" : state.Item2 ? "allowed" : "forbidden");
Open C# practice

C# practice output

unauthenticated
forbidden
allowed
Try this
A development header is only a teaching principal; authentication and role policy remain distinct decisions.

Further reading