Lab 3: Customers and Baristas
45 minutes · Starts from checkpoint-2 · Ends at checkpoint-3
You add real users, issue tokens, and lock down every endpoint so customers and staff can each do exactly what they should.
Step 1: Identity with the EF Core store
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Create Models/RoasteryUser.cs:
public class RoasteryUser : IdentityUser
{
public int? CustomerId { get; set; }
}
Change RoasteryDbContext to inherit from IdentityDbContext<RoasteryUser> and register Identity:
builder.Services.AddIdentityCore<RoasteryUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<RoasteryDbContext>()
.AddApiEndpoints();
Add a migration (AddIdentity) and update the database. Extend the seed to create the Staff role and two users: barista@roastery.dev in the Staff role, and sam@example.com linked to customer 1. Passwords are in src/README.md.
Map Identity's endpoints under /api/auth:
app.MapGroup("/api/auth").MapIdentityApi<RoasteryUser>().WithTags("Auth");
Run the app and send the /api/auth/login request in module-3.http. You get a bearer token back. It works, but it does not carry the claims we need, so keep going.
Step 2: Issue a JWT with roles and customer id
Create Options/JwtOptions.cs (Issuer, Audience, SigningKey) and bind it from the Jwt section. Put the issuer and audience in appsettings.json and the key in user secrets:
dotnet user-secrets init
dotnet user-secrets set "Jwt:SigningKey" "roastery-dev-signing-key-change-me-32-bytes!!"
Add Endpoints/AuthEndpoints.cs with POST /api/auth/token using the IssueToken handler from the concepts page.
Send the two token requests in the .http file, one per seed user. The file captures each token into a variable (@customerToken, @staffToken) that later requests use automatically. Paste one token into jwt.io and confirm the role and customer_id claims are present.
Step 3: Validate tokens and declare policies
Add the AddAuthentication().AddJwtBearer(...) block from the concepts page, then:
builder.Services.AddAuthorizationBuilder()
.AddPolicy("Staff", p => p.RequireRole("Staff"))
.AddPolicy("OwnsOrder", p => p.AddRequirements(new OwnsOrderRequirement()));
builder.Services.AddScoped<IAuthorizationHandler, OwnsOrderHandler>();
app.UseAuthentication();
app.UseAuthorization();
Both Use calls go before the Map calls and after UseHttpsRedirection.
Step 4: Lock it down
Restructure ProductEndpoints into two groups that share a prefix:
var products = app.MapGroup("/api/products").WithTags("Products");
products.MapGet("/", GetProducts);
products.MapGet("/{id:int}", GetProduct);
var staff = products.MapGroup("/").RequireAuthorization("Staff");
staff.MapPost("/", CreateProduct);
staff.MapPut("/{id:int}", UpdateProduct);
staff.MapDelete("/{id:int}", DeleteProduct);
In OrderEndpoints:
- The whole group gets
.RequireAuthorization()(any authenticated user). PUT /{id}/statusadditionally gets.RequireAuthorization("Staff").CreateOrdertakes aClaimsPrincipal userparameter and readscustomer_idfrom it. Delete the hard-codedCustomerId = 1.GetOrderusesIAuthorizationServicewith theOwnsOrderpolicy as shown on the concepts page.GET /api/orders(the list) filters to the caller's own orders unless the caller isStaff.
Step 5: Rate limiting, CORS, and headers
Add the AddRateLimiter configuration from the concepts page, app.UseRateLimiter() after UseHttpsRedirection, and .RequireRateLimiting("auth") on the auth group.
Add CORS with a named policy that allows only https://localhost:5173 (the address of the workshop's small demo front end), with app.UseCors("Frontend").
Add Middleware/SecurityHeadersMiddleware.cs:
app.Use(async (ctx, next) =>
{
ctx.Response.Headers["X-Content-Type-Options"] = "nosniff";
ctx.Response.Headers["Referrer-Policy"] = "no-referrer";
ctx.Response.Headers["X-Frame-Options"] = "DENY";
ctx.Response.Headers.Remove("Server");
await next();
});
Wrap app.UseHsts() in if (!app.Environment.IsDevelopment()).
Step 6: Prove it
Send the verification block in module-3.http. Every request has its expected status in a comment:
Discuss with your neighbor
Why 404 rather than 403 for another customer's order? Would you make the same choice for PUT /api/orders/{id}/status? There is a reasonable argument each way; be ready to say which you picked and why.
Stretch goals
- Wire up Identity's
/api/auth/refreshand have the.httpfile refresh@customerTokenwhen it expires. Shorten the JWT lifetime to two minutes to test it. - Add an
ApiKeyauthentication scheme for a partner integration (a header, a lookup in configuration) and show that a single endpoint can accept either scheme with.RequireAuthorization(p => p.AddAuthenticationSchemes("Bearer", "ApiKey").RequireAuthenticatedUser()). - Replace
RequireRole("Staff")with a claims-basedpermissionscheme (orders:write,products:write) and issue the claims in the token.
Common pitfalls
- Everything returns 401 even with a token.
UseAuthorizationis beforeUseAuthentication, or theUsecalls come after theMapcalls. IDX10653: The encryption algorithm … key size. The signing key is shorter than 32 bytes.- Some staff endpoints are open. You applied
RequireAuthorizationto individual endpoints and missed one. Apply it to the group. - Orders belong to the wrong customer. A
customerIdis still being read from the request body.
Checkpoint
git add -A && git commit -m "Lab 3 complete"
# or: git checkout checkpoint-3
Next: Module 4: Performance, Resilience, and Testing