Every page for logged-in users starts the same way: find the user from the token, and answer 401 when there is none. Copying those lines into every route works until the day you forget them on one route, and that route is open to everybody. An endpoint filter writes the check once, and one line puts it in front of a whole group of routes.
A filter is a method that runs before a route. It gets the request in invocation and the rest of the work in next: it either returns an answer itself, or returns await next(invocation), which runs the route. Here is a small one that has nothing to do with logins: it prints the path of each request before running its route.
async ValueTask<object> LogCalls(EndpointFilterInvocationContext invocation, EndpointFilterDelegate next) {
Console.WriteLine("calling " + invocation.HttpContext.Request.Path);
return await next(invocation);
}
var logged = app.MapGroup("");
logged.AddEndpointFilter(LogCalls);
logged.MapGet("/hello", () => "Hello!");
app.MapGroup("") makes a group of routes, and AddEndpointFilter puts the filter in front of every route mapped on it. The filter can't pass the user to the route as an argument, so it keeps it in HttpContext.Items, a dictionary that lives as long as one request. A route that takes HttpContext context reads it back from context.Items["user"]. Items keeps its values as object, so cast to Dictionary<string, object> to read the email.
Ana (id 1) signs up and logs in first; <token> is the token that login answered:
POST /register {"email": "ana@example.com", "password": "secret123"} → 201 {"email": "ana@example.com", "id": 1}
POST /login {"email": "ana@example.com", "password": "secret123"} → 200 {"token": "<token>"}
GET /me Authorization: Bearer <token> → 200 {"email": "ana@example.com", "id": 1}
GET /me (no header) → 401 {"error": "Login required"}
GET /welcome Authorization: Bearer <token> → 200 Welcome back, ana@example.com!
GET /welcome (no header) → 401 {"error": "Login required"}
GET /welcome Authorization: Bearer 0123456789abcdef0123456789abcdef → 401 {"error": "Login required"}
Your task: below CurrentUser, write the filter LoginRequired: it answers 401 "Login required" when CurrentUser finds nobody, and otherwise keeps the user in Items["user"] and runs the route. Add it to the group loggedIn, then map on that group GET /me, which answers the user, and GET /welcome, which answers the text Welcome back, <email>!.
Assignment
Follow the Coding Tutorial to write the LoginRequired filter and protect /me and /welcome with it.
Hint
The filter has two returns: the 401 answer, and return await next(invocation); after it keeps the user. Map the routes with loggedIn.MapGet, not app.MapGet, or the filter won't run for them.
Free plan: 30 tutor messages. PRO is unlimited.
