-
Notifications
You must be signed in to change notification settings - Fork 371
Add securing-aspnetcore-apis skill (+7.0% eval, near-miss) #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| --- | ||
| name: securing-aspnetcore-apis | ||
| description: Secure ASP.NET Core APIs with authentication, authorization, JWT bearer tokens, CORS configuration, and rate limiting. Use when adding security to web APIs, configuring auth middleware, or fixing common security misconfigurations. | ||
| --- | ||
|
|
||
| # Securing ASP.NET Core APIs | ||
|
|
||
| ## When to Use | ||
|
|
||
| - Adding authentication/authorization to an ASP.NET Core API | ||
| - Configuring JWT bearer token validation | ||
| - Setting up CORS policies for browser clients | ||
| - Implementing rate limiting to prevent abuse | ||
| - Fixing security misconfigurations | ||
|
|
||
| ## When Not to Use | ||
|
|
||
| - The user is building a server-rendered MVC app with cookie auth (different patterns) | ||
| - The app is internal-only behind a service mesh that handles auth | ||
| - The user needs OAuth provider setup (IdP-specific, not general .NET) | ||
|
|
||
| ## Inputs | ||
|
|
||
| | Input | Required | Description | | ||
| |-------|----------|-------------| | ||
| | ASP.NET Core project | Yes | The API project to secure | | ||
| | Auth requirements | No | JWT, API key, OAuth, or mixed | | ||
|
|
||
| ## Workflow | ||
|
|
||
| ### Step 1: Add JWT Bearer authentication | ||
|
|
||
| ```bash | ||
| dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer | ||
| ``` | ||
|
|
||
| ```csharp | ||
| builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) | ||
| .AddJwtBearer(options => | ||
| { | ||
| options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0"; | ||
| options.Audience = "api://{client-id}"; | ||
|
|
||
| // CRITICAL: Do NOT disable these in production | ||
| options.TokenValidationParameters = new TokenValidationParameters | ||
| { | ||
| ValidateIssuer = true, | ||
| ValidateAudience = true, | ||
| ValidateLifetime = true, | ||
| ValidateIssuerSigningKey = true, | ||
| ClockSkew = TimeSpan.FromMinutes(5) // default; resist reducing to 0 | ||
| }; | ||
| }); | ||
|
|
||
| builder.Services.AddAuthorization(); | ||
| ``` | ||
|
|
||
| ### Step 2: Configure middleware in the CORRECT order | ||
|
|
||
| **Middleware order is critical. Wrong order = auth bypassed silently.** | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we refer to https://learn.microsoft.com/aspnet/core/fundamentals/middleware/?view=aspnetcore-10.0#middleware-order so we don't need to maintain an explicit list here? Or add it as an extra resource to check if needed? |
||
|
|
||
| ```csharp | ||
| var app = builder.Build(); | ||
|
|
||
| // 1. Exception handling first (catches errors from all middleware) | ||
| app.UseExceptionHandler("/error"); | ||
|
|
||
| // 2. HTTPS redirection | ||
| app.UseHttpsRedirection(); | ||
|
|
||
| // 3. CORS — MUST be before auth for preflight requests to work | ||
| app.UseCors(); | ||
|
|
||
| // 4. Authentication — MUST be before Authorization | ||
| app.UseAuthentication(); | ||
|
|
||
| // 5. Authorization — MUST be after Authentication | ||
| app.UseAuthorization(); | ||
|
|
||
| // 6. Rate limiting — after auth so you can rate-limit per user | ||
| app.UseRateLimiter(); | ||
|
|
||
| // 7. Endpoints | ||
| app.MapControllers(); | ||
| ``` | ||
|
|
||
| **Common mistake:** Putting `UseAuthorization()` before `UseAuthentication()` — auth checks run but identity is never set, so everything returns 401. | ||
|
|
||
| ### Step 3: Apply authorization policies | ||
|
|
||
| **Per-endpoint (Minimal APIs):** | ||
| ```csharp | ||
| app.MapGet("/api/orders", GetOrders) | ||
| .RequireAuthorization(); | ||
|
|
||
| app.MapDelete("/api/orders/{id}", DeleteOrder) | ||
| .RequireAuthorization("AdminOnly"); | ||
| ``` | ||
|
|
||
| **Policy-based authorization:** | ||
| ```csharp | ||
| builder.Services.AddAuthorization(options => | ||
| { | ||
| options.AddPolicy("AdminOnly", policy => | ||
| policy.RequireRole("Admin")); | ||
|
|
||
| options.AddPolicy("CanManageOrders", policy => | ||
| policy.RequireClaim("permission", "orders.write")); | ||
|
|
||
| // Fallback policy — applies to ALL endpoints without explicit auth | ||
| options.FallbackPolicy = new AuthorizationPolicyBuilder() | ||
| .RequireAuthenticatedUser() | ||
| .Build(); | ||
| }); | ||
| ``` | ||
|
|
||
| **IMPORTANT:** Setting `FallbackPolicy` makes ALL endpoints require auth by default. Explicitly allow anonymous where needed: | ||
|
|
||
| ```csharp | ||
| app.MapGet("/health", () => "OK").AllowAnonymous(); | ||
| app.MapPost("/api/auth/login", Login).AllowAnonymous(); | ||
| ``` | ||
|
|
||
| ### Step 4: Configure CORS correctly | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth mentioning Websocket origin checks https://learn.microsoft.com/aspnet/core/fundamentals/websockets?view=aspnetcore-10.0#websocket-origin-restriction |
||
|
|
||
| ```csharp | ||
| builder.Services.AddCors(options => | ||
| { | ||
| options.AddPolicy("Production", policy => | ||
| { | ||
| policy.WithOrigins( | ||
| "https://app.example.com", | ||
| "https://admin.example.com") | ||
| .WithMethods("GET", "POST", "PUT", "DELETE") | ||
| .WithHeaders("Authorization", "Content-Type") | ||
| .AllowCredentials(); // Required if frontend sends cookies/tokens | ||
| }); | ||
| }); | ||
|
|
||
| // Apply globally | ||
| app.UseCors("Production"); | ||
| ``` | ||
|
|
||
| **NEVER do this in production:** | ||
| ```csharp | ||
| // INSECURE — allows any origin to call your API | ||
| policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another insecure pattern is |
||
| ``` | ||
|
|
||
| ### Step 5: Add rate limiting (.NET 7+) | ||
|
|
||
| ```csharp | ||
| builder.Services.AddRateLimiter(options => | ||
| { | ||
| // Global limiter | ||
| options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>( | ||
| context => RateLimitPartition.GetFixedWindowLimiter( | ||
| partitionKey: context.User?.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider different partitions for anonymous vs. authenticated users. |
||
| factory: _ => new FixedWindowRateLimiterOptions | ||
| { | ||
| PermitLimit = 100, | ||
| Window = TimeSpan.FromMinutes(1), | ||
| QueueLimit = 0 | ||
| })); | ||
|
|
||
| options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; | ||
| }); | ||
| ``` | ||
|
|
||
| ### Step 6: Security headers | ||
|
|
||
| ```csharp | ||
| app.Use(async (context, next) => | ||
| { | ||
| context.Response.Headers.Append("X-Content-Type-Options", "nosniff"); | ||
| context.Response.Headers.Append("X-Frame-Options", "DENY"); | ||
| context.Response.Headers.Append("X-XSS-Protection", "0"); // Modern browsers don't need it | ||
| context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin"); | ||
| context.Response.Headers.Append("Content-Security-Policy", "default-src 'self'"); | ||
| await next(); | ||
| }); | ||
| ``` | ||
|
|
||
| ## Security Checklist | ||
|
|
||
| - [ ] `UseAuthentication()` comes BEFORE `UseAuthorization()` | ||
| - [ ] JWT validation checks issuer, audience, lifetime, and signing key | ||
| - [ ] CORS `WithOrigins` lists specific origins (not `AllowAnyOrigin`) | ||
| - [ ] All endpoints require auth by default (FallbackPolicy) | ||
| - [ ] Health/login endpoints explicitly marked `AllowAnonymous` | ||
| - [ ] Rate limiting enabled with per-user partitioning | ||
| - [ ] HTTPS enforced with `UseHttpsRedirection` | ||
| - [ ] No secrets in `appsettings.json` (use user-secrets or Key Vault) | ||
|
|
||
| ## Common Pitfalls | ||
|
|
||
| | Pitfall | Solution | | ||
| |---------|----------| | ||
| | Auth middleware order wrong | Auth → Authz, always in that order. CORS before both | | ||
| | `AllowAnyOrigin` in production | Whitelist specific origins | | ||
| | JWT secret in appsettings.json | Use environment variables, user-secrets, or Azure Key Vault | | ||
| | 401 instead of 403 | 401 = not authenticated; 403 = authenticated but not authorized. Check claims | | ||
| | CORS preflight failures | Browser sends OPTIONS; ensure CORS middleware handles it before auth | | ||
| | Rate limiter not per-user | Partition by user identity OR IP, not globally | | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| scenarios: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume there will be more scenarios in the future, this is just a good first start? |
||
| - name: "Fix CORS preflight 401 with JWT auth middleware ordering" | ||
| prompt: | | ||
| I set up JWT authentication and CORS on my ASP.NET Core 8 API but the React SPA on https://myapp.example.com keeps getting 401 errors on preflight OPTIONS requests. Here is my middleware pipeline: | ||
|
|
||
| ```csharp | ||
| var app = builder.Build(); | ||
| app.UseHttpsRedirection(); | ||
| app.UseAuthentication(); | ||
| app.UseAuthorization(); | ||
| app.UseCors("AllowSPA"); | ||
| app.UseRateLimiter(); | ||
| app.MapControllers(); | ||
| app.Run(); | ||
| ``` | ||
|
|
||
| And CORS is configured as: | ||
| ```csharp | ||
| builder.Services.AddCors(options => | ||
| { | ||
| options.AddPolicy("AllowSPA", b => b | ||
| .AllowAnyOrigin() | ||
| .AllowAnyMethod() | ||
| .AllowAnyHeader() | ||
| .AllowCredentials()); | ||
| }); | ||
| ``` | ||
|
|
||
| The JWT auth works fine when I test directly from Postman. What's wrong? | ||
| assertions: | ||
| - type: "output_matches" | ||
| pattern: "(UseCors.*before.*UseAuthentication|middleware.*order|order.*middleware|UseCors.*UseAuth)" | ||
| - type: "output_matches" | ||
| pattern: "(WithOrigins|specific.*origin|AllowAnyOrigin.*AllowCredentials.*conflict|cannot.*AllowAnyOrigin.*AllowCredentials)" | ||
| rubric: | ||
| - "Identified that UseCors must come BEFORE UseAuthentication so OPTIONS preflight requests are handled before auth rejects them" | ||
| - "Showed the correct middleware order: UseCors → UseAuthentication → UseAuthorization → UseRateLimiter" | ||
| - "Identified the AllowAnyOrigin + AllowCredentials conflict (CORS spec forbids this combination)" | ||
| - "Fixed CORS to use WithOrigins(\"https://myapp.example.com\") instead of AllowAnyOrigin when using AllowCredentials" | ||
| - "Explained why OPTIONS requests fail: browsers send preflight with no auth token, so auth middleware returns 401 before CORS handles it" | ||
| - "Provided the corrected full middleware pipeline code" | ||
| expect_tools: ["bash"] | ||
| timeout: 120 | ||
|
|
||
| - name: "Security skill should not activate for internal service question" | ||
| prompt: "I have an internal gRPC service running in Kubernetes behind a service mesh. How do I set up communication between my two microservices?" | ||
| assertions: | ||
| - type: "output_not_contains" | ||
| value: "JwtBearer" | ||
| - type: "output_not_matches" | ||
| pattern: "(AddAuthentication|JWT|CORS|rate.limit)" | ||
| rubric: | ||
| - "Did NOT suggest JWT/CORS/rate limiting for an internal service mesh question" | ||
| - "Provided gRPC or service mesh relevant guidance" | ||
| timeout: 60 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The skill is about multiple different security related concepts, some of which likely still apply to the items in this 'Not' section.