Learnixo
Back to blog
AI Systemsintermediate

Authentication and Authorization in Minimal APIs

Apply JWT authentication and policy-based authorization to Minimal API endpoints: RequireAuthorization, route groups with shared auth, resource-based authorization, and the patterns that secure clinical APIs.

Asma Hafeez KhanMay 16, 20264 min read
Minimal APIsAuthenticationAuthorizationASP.NET Core.NET
Share:𝕏

Auth in Minimal APIs

Authentication and authorization in Minimal APIs use the same system as controllers — same AddJwtBearer, same policies, same IAuthorizationHandler. The difference is where you apply it: .RequireAuthorization() on endpoints or groups instead of [Authorize] attributes.


Setup

C#
// Program.cs — register auth
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer           = true,
            ValidateAudience         = true,
            ValidateLifetime         = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer              = builder.Configuration["Jwt:Issuer"],
            ValidAudience            = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey         = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
            ClockSkew                = TimeSpan.FromSeconds(30),
        };
    });

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("DoctorsOnly",      p => p.RequireRole("Doctor"))
    .AddPolicy("PharmacyStaff",    p => p.RequireRole("Pharmacist", "PharmacyAdmin"))
    .AddPolicy("ClinicalStaff",    p => p.RequireRole("Doctor", "Nurse", "Pharmacist"))
    .AddPolicy("AdminOnly",        p => p.RequireRole("Admin"))
    .AddPolicy("ScheduleIIPrescriber", p =>
        p.RequireRole("Doctor")
         .RequireClaim("schedule_ii_prescriber", "true"));

// Middleware — order matters
app.UseAuthentication();
app.UseAuthorization();

Endpoint-Level Authorization

C#
// No auth required
app.MapGet("/health", () => Results.Ok()).AllowAnonymous();
app.MapPost("/auth/login", Login).AllowAnonymous();

// Any authenticated user
app.MapGet("/profile", GetProfile).RequireAuthorization();

// Specific role
app.MapPost("/prescriptions", CreatePrescription)
    .RequireAuthorization("DoctorsOnly");

// Multiple policies (AND — all must pass)
app.MapPost("/prescriptions/schedule-ii", CreateScheduleIIPrescription)
    .RequireAuthorization("DoctorsOnly")
    .RequireAuthorization("ScheduleIIPrescriber");

Route Groups with Auth

C#
// Apply auth at group level — inherited by all endpoints
var publicRoutes  = app.MapGroup("/api/public").AllowAnonymous();
var authRoutes    = app.MapGroup("/api").RequireAuthorization();
var doctorRoutes  = app.MapGroup("/api/clinical").RequireAuthorization("DoctorsOnly");
var pharmacyRoutes = app.MapGroup("/api/pharmacy").RequireAuthorization("PharmacyStaff");
var adminRoutes   = app.MapGroup("/api/admin").RequireAuthorization("AdminOnly");

// Override policy on a specific endpoint within a group
doctorRoutes.MapGet("/patients/{id}", GetPatient)
    .RequireAuthorization("ClinicalStaff");  // override — nurses can also view

Global Fallback Policy

C#
// All endpoints require auth by default — opt out with AllowAnonymous
builder.Services.AddAuthorizationBuilder()
    .SetFallbackPolicy(new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build());

// Now you cannot forget RequireAuthorization — it is the default
// Explicitly opt out for public endpoints:
app.MapGet("/health", () => Results.Ok("healthy")).AllowAnonymous();
app.MapPost("/auth/login", Login).AllowAnonymous();
app.MapGet("/openapi/v1.json", ServeOpenApi).AllowAnonymous();

Reading Claims in Handlers

C#
// ClaimsPrincipal parameter — injected automatically for authenticated requests
app.MapGet("/my-patients", async (
    ClaimsPrincipal user,
    PatientRepository repo,
    CancellationToken ct) =>
{
    var doctorId = user.FindFirstValue(JwtRegisteredClaimNames.Sub);
    if (doctorId is null) return Results.Unauthorized();

    var patients = await repo.GetByDoctorIdAsync(Guid.Parse(doctorId), ct);
    return Results.Ok(patients);
}).RequireAuthorization("DoctorsOnly");

// Or use ICurrentUser abstraction
app.MapGet("/my-patients", async (
    ICurrentUser currentUser,
    PatientRepository repo,
    CancellationToken ct) =>
{
    var patients = await repo.GetByDoctorIdAsync(currentUser.Id, ct);
    return Results.Ok(patients);
}).RequireAuthorization("DoctorsOnly");

Resource-Based Authorization

C#
// Authorization depends on the resource — not just the user's identity
app.MapGet("/patients/{id:guid}/records", async (
    Guid id,
    IAuthorizationService authz,
    ClaimsPrincipal user,
    PatientRepository repo,
    CancellationToken ct) =>
{
    var patient = await repo.GetByIdAsync(id, ct);
    if (patient is null) return Results.NotFound();

    // Check: can this user access THIS patient (same department?)
    var authResult = await authz.AuthorizeAsync(user, patient, "SameDepartment");
    if (!authResult.Succeeded) return Results.Forbid();

    return Results.Ok(patient.ToDetailedDto());
}).RequireAuthorization();

Returning the Right Status Codes

C#
// 401 Unauthorized: not authenticated (no token or invalid token)
// 403 Forbidden: authenticated but not authorized (right user, wrong role/resource)

// The RequireAuthorization() middleware handles 401 automatically
// For 403 in resource-based auth:
if (!authResult.Succeeded)
    return Results.Forbid();  // 403 — authenticated but not allowed

// Do not return 404 instead of 403 for security reasons
// Returning 404 for "patient exists but you cannot access it" leaks
// the existence of the resource to unauthorized users
// Exception: clinical systems where "I know this patient exists" is also sensitive
//            → return 404 even for authorized patients in some contexts

Audit Logging for Clinical Endpoints

C#
// Apply audit logging to all clinical endpoints via group filter
var clinicalGroup = app.MapGroup("/api/clinical")
    .RequireAuthorization("ClinicalStaff")
    .AddEndpointFilter<ClinicalAuditFilter>();

public sealed class ClinicalAuditFilter : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext ctx,
        EndpointFilterDelegate next)
    {
        var user    = ctx.HttpContext.User;
        var userId  = user.FindFirstValue(JwtRegisteredClaimNames.Sub);
        var path    = ctx.HttpContext.Request.Path;
        var method  = ctx.HttpContext.Request.Method;

        // Log access BEFORE the handler — captures intent even if handler crashes
        ctx.HttpContext.RequestServices
            .GetRequiredService<IAuditLogger>()
            .LogClinicalAccess(userId!, method, path);

        return await next(ctx);
    }
}

Red Flag / Green Answer

Red Flag: "We handle 401 and 403 both by returning Results.Unauthorized() (401) everywhere, including when a user is authenticated but lacks access to a specific resource."

Mixing 401 and 403 breaks client applications. 401 means "you need to authenticate." Most client apps respond to 401 by redirecting to the login page. An authenticated user who lacks permission should get 403 — not be redirected to login.

Green Answer:

RequireAuthorization() returns 401 for unauthenticated requests automatically. Return Results.Forbid() (403) when the user is authenticated but fails authorization. Client apps distinguish: 401 → redirect to login, 403 → show "access denied" message.


Key Takeaway

Minimal API authorization uses the same policies and handlers as MVC controllers. Apply .RequireAuthorization() per endpoint or per MapGroup. Use the fallback policy to require auth everywhere — opt out explicitly with .AllowAnonymous(). For resource-based authorization, inject IAuthorizationService and call AuthorizeAsync with the resource. Return 401 for unauthenticated, 403 for unauthorized — they are different conditions with different client responses.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.