Reversed Auth Middleware Order — Silent 401 in ASP.NET Core
A valid JWT yields 401 when UseAuthorization runs before UseAuthentication.
20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.
- ASP.NET Core middleware is a chain of delegates that process HTTP requests and responses in a two-phase pipeline.
- Each middleware can inspect, modify, or short-circuit the request/response.
- Order is critical: each middleware depends on the layer above having run.
- Performance overhead is minimal (sub-millisecond per middleware) but incorrect order silently breaks security.
- Production insight: Putting UseAuthorization before UseAuthentication returns 401 on every authenticated request.
- Biggest mistake: Injecting a scoped DbContext into the middleware constructor instead of InvokeAsync parameters.
Imagine every HTTP request is a letter arriving at a busy office. Before the letter reaches the CEO (your controller), it passes through a chain of desks — the receptionist checks if it's addressed correctly, the security guard checks for a valid ID badge, the assistant stamps it with the time it arrived. Each desk can either pass the letter forward, send it straight back, or even modify it before passing it on. That chain of desks is exactly what ASP.NET Core middleware is. Each piece of middleware is one desk in that chain — it inspects the request, optionally does something to it, and decides whether to pass it to the next desk or short-circuit and send a response right back.
Every web framework needs a way to handle the messy, repetitive work that surrounds every request — logging it, authenticating the caller, compressing the response, catching exceptions. Without a clean mechanism for this, you'd be copy-pasting the same authentication check into every single controller action. That's not just tedious; it's a reliability disaster. ASP.NET Core solves this with a first-class middleware pipeline that's fast, composable, and completely in your control.
The problem middleware solves is cross-cutting concerns. Authentication doesn't belong to any one feature — it belongs to all of them. The same goes for logging, CORS headers, rate limiting, and exception handling. Middleware lets you write that logic once, plug it into the pipeline in the right place, and trust that it runs for every request that flows through your app. The pipeline model also means concerns are layered cleanly: outer middleware wraps inner middleware, so you can handle exceptions around everything, authenticate before authorisation, and authorise before routing — all without tangling those concerns together.
By the end of this article you'll be able to draw the ASP.NET Core request pipeline from memory, explain why order matters (with a concrete example of what breaks when you get it wrong), write a custom middleware class with proper dependency injection, and use the three ways to register middleware — UseMiddleware, Use, and Map — choosing the right tool for each situation.
What Middleware in ASP.NET Core Actually Does
Middleware in ASP.NET Core is software assembled into an application pipeline to handle requests and responses. Each component chooses whether to pass the request to the next component and can perform actions before and after the next component in the pipeline. This is the core mechanic: a chain of delegates where each middleware can short-circuit the pipeline by not calling the next delegate.
In practice, middleware is registered in order in the Program.cs file using extension methods like UseAuthorization(), UseAuthentication(), and UseCors(). The order matters critically because each middleware can modify the request or response context. For example, authentication middleware must run before authorization middleware — otherwise, authorization checks will see an unauthenticated user and fail silently, returning a 401 without any diagnostic.
You use middleware to handle cross-cutting concerns like logging, exception handling, authentication, authorization, response compression, and static files. In real systems, getting the order wrong is the most common source of silent 401 errors that baffle teams. The rule is: CORS, then Authentication, then Authorization, then your custom middleware — in that exact sequence.
How the ASP.NET Core Request Pipeline Actually Works
The pipeline is a linked chain of delegates. When a request arrives, ASP.NET Core calls the first delegate. That delegate can do work, then call 'next()' to invoke the next delegate in the chain, then do more work after next() returns. This means every piece of middleware has two opportunities to act: once on the way in (before calling next) and once on the way out (after next returns). That's the key mental model — it's not a one-way conveyor belt, it's a stack of nested function calls.
The chain terminates at a 'terminal middleware' — something like UseEndpoints — that handles the request and writes a response without calling next. If no terminal middleware matches, ASP.NET Core returns a 404.
Each middleware is registered in Program.cs using extension methods on WebApplication. The order you call those methods is the order they execute. This isn't a detail — it's the most important rule in ASP.NET Core middleware. Authentication must come before Authorisation. Exception handling must wrap everything else. We'll see exactly what breaks when that order is wrong in the Gotchas section.
next() runs after all inner middleware have completed.Writing a Real Custom Middleware Class (With Dependency Injection)
Inline lambdas with app.Use() are fine for trivial cases, but anything non-trivial should be a dedicated class. A middleware class gives you a testable unit, proper constructor injection, and a home for the logic that doesn't clutter Program.cs.
The convention ASP.NET Core uses for middleware classes is simple: your class needs a constructor that accepts a RequestDelegate (the next middleware), and an InvokeAsync method that accepts HttpContext. That's the entire contract. You don't implement any interface — the framework discovers the method by convention.
Scoped services can't be injected via the constructor because middleware is instantiated once (singleton lifetime). Instead, inject scoped services as parameters of InvokeAsync — the framework handles that automatically. This is a common interview question and a common production bug. The example below builds a request-timing middleware that logs how long each request takes, which is something almost every production app needs.
Map, Use, and Run — Choosing the Right Registration Method
ASP.NET Core gives you three core extension methods for building the pipeline, and each has a specific job. Mixing them up is a common source of subtle bugs.
'app.Use()' is the standard building block. It receives the HttpContext and a delegate to the next middleware. You call next to pass control forward. Use it for any middleware that should participate in both the incoming and outgoing phases.
'app.Run()' registers a terminal middleware — it never calls next because it's the end of the line. If you use Run and then register more middleware after it, those later registrations are silently ignored. This surprises a lot of developers.
'app.Map()' branches the pipeline based on the request path. Requests matching the path go down the branch; everything else continues on the main pipeline. It's perfect for health check endpoints, admin sections, or versioned API branches that need completely different middleware stacks. There's also 'MapWhen()' for branching on arbitrary conditions, like a header value or query string.
Map() when a section of your app needs genuinely different middleware. A /metrics endpoint, for example, shouldn't go through authentication middleware — it should have its own IP-allow-list check. Map lets you build a completely separate pipeline branch rather than cluttering your main pipeline with conditional logic.Run() is terminal—any middleware registered after it is silently ignored.Run() and wondering why it doesn't execute.Run() for the final handler; use app.Use() for middleware that should continue.Middleware Order in Practice — The Correct ASP.NET Core Pipeline
The single most confusing thing about ASP.NET Core middleware for developers coming from other frameworks is that order is everything, and it's silent about it. There's no error if you put UseAuthorization before UseAuthentication — it just doesn't work. You get 401s or 403s that seem random until you understand the pipeline.
Microsoft documents a recommended order and it exists for good reason. Exception handling must wrap everything to catch exceptions from routing, authentication, and your own code. HTTPS redirection should happen before static files so you don't serve static assets over HTTP. Authentication must happen before Authorisation because Authorisation reads the identity that Authentication establishes. Routing must happen before Authorisation so the framework knows which endpoint's policies to check.
The table below compares the correct order against a common incorrect ordering and shows exactly what symptom you'd see. Once you understand the 'why' behind each position, the order becomes easy to remember — it's not arbitrary, it's causal.
Short-Circuiting the Pipeline: When to Return Early Without Calling next()
Middleware isn't required to call next(). If it sets a response and returns without calling next, the pipeline short-circuits — inner middleware never runs, and outer middleware's post-next code also doesn't execute. This is exactly what authentication middleware does when a token is invalid: it returns 401 immediately without passing the request to your controllers.
Short-circuiting is powerful for early rejection patterns: maintenance mode checks, IP whitelisting, request size limits, and authentication failures all legitimately stop the pipeline early. But there's a trade-off: if you have timing or logging middleware registered before the short-circuiting middleware, their outgoing-phase code won't run. That means no log entry for rejected authentication attempts unless the short-circuiting middleware handles logging itself.
The decision to short-circuit should be deliberate. Use it when you know the request cannot possibly be fulfilled. But if you only want to modify the response (e.g., add a header) without affecting inner pipeline logic, always call next() first and modify the response after.
next()) never executes for requests that are short-circuited. Your monitors will miss those requests. To capture them, either: (1) register logging middleware after the short-circuit middleware, or (2) have the short-circuit middleware manually emit its own log entry before returning.next() to continue pipeline, or skip to short-circuit.Middleware Dependencies — Singleton, Scoped, Transient: Choose or Lose
Middleware constructors are singleton by default. They live for the lifetime of the application. That means any scoped service injected via the constructor is effectively a singleton — you'll get the same instance for every request.
This is a production trap. You register a scoped IInvoiceContext, inject it into middleware, and wonder why your second request has stale data. Because it does. The DI container resolves scoped services at startup when the middleware is instantiated, not per request.
InvokeAsync takes a scoped parameter directly from DI for each request. Use that pattern. Never inject scoped services into the constructor. Transient services that carry state? Same rule. Constructor is for singletons only. If you need a fresh instance of something per request, pull it in the method signature.
Here's the litmus test: if your middleware touches a database, reads user-specific data, or writes to a scoped cache, it belongs in InvokeAsync, not the constructor. If it's static configuration or a logger? Fine in the constructor. Everything else is a bug waiting to happen.
Branch the Pipeline with Map — Routes, Not Religion
Not every path through your app needs the same middleware. Logging headers? Yes, everywhere. Checking authentication for the admin panel? Only under /admin. Rendering static files? Only for paths that map to actual files.
Branching middleware pipelines is how you avoid paying for work you don't need. Map splits the pipeline based on a path match. When the request hits /healthz, you don't need rate limiting, auth, or request logging — just return 200 and get out.
MapWhen gives you more control — predicate on anything: query string, header value, random coin flip. But don't get cute. Branching should reflect explicit app boundaries: admin area, API version, health probes. Use Map for static path branches, MapWhen only when you absolutely need conditional logic.
Every branch is a fresh pipeline. That means if you branch after CORS and before auth, the branch doesn't get auth. Plan your branch points carefully. The middleware order rules don't change per branch — each branch is independent. Common pitfall: branching early and forgetting to re-add error handling. Your health check gets no exception handler. One unhandled exception later, you remember.
If a branch doesn't call _next, it's terminal. That's the whole point.
Map for path-based branches — it's cleaner and the pipeline abstraction is obvious. MapWhen is for runtime conditions. If you use MapWhen for something that could be Map, you're making the team guess what the predicate does.The Silent 401: When Middleware Order Strikes at 3 AM
UseAuthorization() was placed before UseAuthentication(). The authorization middleware checked context.User, which was still an anonymous identity because authentication hadn't run yet. No exception, no warning — every request that hit an [Authorize] endpoint returned 401.UseAuthentication() immediately before UseAuthorization(). The recommended pipeline: exception handler → routing → cors → authentication → authorization → endpoints. Deployed the fix and all endpoints returned 200.- The order of middleware is causal, not decorative: authentication builds the user identity; authorization reads it. Reversing them is a silent failure.
- Always run a simple integration test after any middleware registration change — send a request with a valid token and assert the response is not 401.
- Use code analysis or a custom middleware that logs the pipeline order at startup to catch accidental reordering.
Run() call appears before it. app.Run() is terminal — everything after it is silently ignored. Ensure non-terminal middleware uses app.Use() not app.Run().UseAuthentication() must be before UseAuthorization(). Also confirm that CORS middleware is after UseRouting and before UseAuthentication.UseCors() is placed after UseRouting() and before UseAuthentication(). Preflight requests should not require authentication. Also verify the CORS policy allows the requested origin and method.curl -v -H 'Authorization: Bearer <token>' http://localhost:5000/api/secure | grep HTTPCheck Program.cs for middleware registration sequence. Use dotnet list configuration to verify appsettings.Key takeaways
next() gets to act on both the incoming request (before next) and the outgoing response (after next). This is how UseExceptionHandler and timing middleware work.Run() silently swallows everything registered after itRun() call appears before it in Program.cs.Common mistakes to avoid
3 patternsPutting UseAuthorization before UseAuthentication
UseAuthentication() immediately before UseAuthorization(). Authentication sets up context.User; Authorisation reads it.Injecting a scoped service (e.g., DbContext) into the middleware constructor
Registering middleware after app.Run()
Run() is completely silently ignored. No warning, no exception.Run() is terminal — it never calls next. Any middleware that should run for all requests must be registered before app.Run(). Use app.Use() for non-terminal middleware and reserve app.Run() for the very last handler only.Interview Questions on This Topic
Can you explain the ASP.NET Core request pipeline and why middleware order matters? Give a concrete example of what breaks when the order is wrong.
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.
That's ASP.NET. Mark it forged?
7 min read · try the examples if you haven't