Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions src/dotnet/skills/securing-aspnetcore-apis/SKILL.md
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

Copy link
Copy Markdown
Member

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.


- 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.**

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


```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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another insecure pattern is policy.SetIsOriginAllowed(origin => return true)

```

### 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",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 |
47 changes: 47 additions & 0 deletions src/dotnet/tests/securing-aspnetcore-apis/eval.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
scenarios:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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: "Secure an ASP.NET Core API with JWT auth and CORS"
prompt: |
I'm building a public ASP.NET Core 8 Web API that will be consumed by a React SPA running on a different domain. I need to:
1. Add JWT bearer authentication
2. Configure CORS properly for the SPA
3. Add rate limiting to prevent abuse

My Program.cs currently has no auth:

```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
```

Show me the correct and secure way to set this up.
assertions:
- type: "output_matches"
pattern: "(AddAuthentication|JwtBearer|JwtBearerDefaults)"
- type: "output_matches"
pattern: "(AddCors|CORS|WithOrigins)"
- type: "output_matches"
pattern: "(UseAuthentication.*UseAuthorization|middleware.order|order.matters)"
rubric:
- "Added JWT bearer auth with proper TokenValidationParameters (ValidateIssuer, ValidateAudience, ValidateLifetime all true)"
- "Configured CORS with specific origins (NOT AllowAnyOrigin with credentials)"
- "Added rate limiting using AddRateLimiter with a fixed or sliding window policy"
- "Placed middleware in correct order: UseCors → UseAuthentication → UseAuthorization → UseRateLimiter"
- "Explicitly stated that middleware order matters and incorrect order silently bypasses security"
- "Did NOT disable token validation parameters or use AllowAnyOrigin in production config"
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
Loading