| name | dotnet-api-security |
| description | Implementing API auth. Identity, OAuth/OIDC, JWT bearer, passkeys (WebAuthn), CORS, rate limiting. |
dotnet-api-security
API-level authentication, authorization, and security patterns for ASP.NET Core. This skill owns API auth implementation: ASP.NET Core Identity configuration, OAuth 2.0/OIDC integration, JWT bearer token handling, passkey (WebAuthn) authentication, CORS policies, Content Security Policy headers, and rate limiting.
Auth ownership: This skill owns API-level auth patterns. Blazor-specific auth UI (AuthorizeView, CascadingAuthenticationState, client-side token handling) -- see [skill:dotnet-blazor-auth] when it lands. OWASP security principles (cross-cutting vulnerability mitigations) -- see [skill:dotnet-security-owasp].
Out of scope: OWASP Top 10 mitigations and deprecated security patterns -- see [skill:dotnet-security-owasp]. Secrets management and secure configuration -- see [skill:dotnet-secrets-management]. Cryptographic algorithm selection -- see [skill:dotnet-cryptography]. Blazor auth UI components -- see [skill:dotnet-blazor-auth].
Cross-references: [skill:dotnet-security-owasp] for OWASP security principles, [skill:dotnet-secrets-management] for secrets handling, [skill:dotnet-cryptography] for cryptographic best practices.
ASP.NET Core Identity
ASP.NET Core Identity provides user management, password hashing, role-based authorization, and two-factor authentication out of the box. It is the recommended starting point for applications that manage their own user accounts.
builder.Services.AddIdentityApiEndpoints<ApplicationUser>(options =>
{
options.Password.RequiredLength = 12;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
options.Password.RequireDigit = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
var app = builder.Build();
app.MapIdentityApi<ApplicationUser>();
Identity API Endpoints (.NET 8+)
MapIdentityApi<TUser>() provides pre-built token-based authentication endpoints for SPAs and mobile clients without Razor UI:
| Endpoint | Method | Description |
|---|
/register | POST | Create a new user account |
/login | POST | Authenticate and receive tokens |
/refresh | POST | Refresh an expired access token |
/confirmEmail | GET | Confirm email address |
/manage/info | GET/POST | Get/update user profile |
/manage/2fa | POST | Configure two-factor authentication |
OAuth 2.0 / OpenID Connect
For applications that delegate authentication to an external identity provider (Entra ID, Auth0, Okta, Keycloak), configure OIDC middleware.
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = builder.Configuration["Oidc:Authority"];
options.ClientId = builder.Configuration["Oidc:ClientId"];
options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "roles";
});
Gotcha: MapInboundClaims = false prevents the Microsoft OIDC handler from remapping standard JWT claims (e.g., sub to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier). Set this to false to preserve the original claim types from the identity provider.
JWT Bearer Token Authentication
For API-only scenarios where the client sends a JWT in the Authorization header:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Jwt:Authority"];
options.Audience = builder.Configuration["Jwt:Audience"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(1)
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/profile", (ClaimsPrincipal user) =>
TypedResults.Ok(new { Name = user.Identity?.Name }))
.RequireAuthorization();
Policy-Based Authorization
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"))
.AddPolicy("PremiumUser", policy =>
policy.RequireClaim("subscription", "premium"))
.SetFallbackPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build());
Passkeys / WebAuthn (.NET 10)
.NET 10 introduces built-in passkey (WebAuthn/FIDO2) support for passwordless authentication. Passkeys use public-key cryptography and are phishing-resistant.
builder.Services.AddIdentityApiEndpoints<ApplicationUser>(options =>
{
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders()
.AddPasskeys();
var app = builder.Build();
app.MapIdentityApi<ApplicationUser>();
Passkey Registration Flow
- Client calls
/passkey/register/options to get a PublicKeyCredentialCreationOptions challenge
- Client creates a credential using the Web Authentication API (
navigator.credentials.create)
- Client sends the attestation response to
/passkey/register
- Server validates and stores the credential
Passkey Authentication Flow
- Client calls
/passkey/login/options to get a PublicKeyCredentialRequestOptions challenge
- Client signs the challenge using
navigator.credentials.get
- Client sends the assertion response to
/passkey/login
- Server validates the assertion and issues a session/token
Key benefits: No passwords to phish, no credentials stored server-side (only public keys), built-in resistance to replay attacks.
CORS Policies
Cross-Origin Resource Sharing (CORS) controls which origins can call your API. Always use explicit, named policies -- never use AllowAnyOrigin() in production.
builder.Services.AddCors(options =>
{
options.AddPolicy("Production", policy =>
{
policy.WithOrigins(
"https://app.example.com",
"https://admin.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization")
.SetPreflightMaxAge(TimeSpan.FromMinutes(10));
});
options.AddPolicy("Development", policy =>
{
policy.WithOrigins("https://localhost:5173")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors(app.Environment.IsDevelopment() ? "Development" : "Production");
Common CORS Pitfalls
AllowAnyOrigin() + AllowCredentials() is rejected at runtime by ASP.NET Core. But SetIsOriginAllowed(_ => true) + AllowCredentials() silently allows all origins -- never use this pattern.
- Preflight caching: Without
SetPreflightMaxAge, browsers send an OPTIONS request before every cross-origin request. Set a reasonable cache duration (10-60 minutes) to reduce latency.
- Wildcard headers with credentials:
AllowAnyHeader() combined with AllowCredentials() works in ASP.NET Core but may behave unexpectedly in some browsers. Prefer explicit header lists.
- CORS middleware order:
UseCors() must be called after UseRouting() and before UseAuthorization().
Content Security Policy (CSP)
Content Security Policy headers prevent XSS, clickjacking, and other injection attacks by controlling which resources the browser can load.
app.Use(async (context, next) =>
{
context.Response.Headers.Append(
"Content-Security-Policy",
"default-src 'none'; frame-ancestors 'none'");
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
context.Response.Headers.Append("X-Frame-Options", "DENY");
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
context.Response.Headers.Append("Permissions-Policy",
"camera=(), microphone=(), geolocation=()");
await next();
});
For APIs serving HTML responses (Razor Pages, Blazor Server), use a more permissive CSP with nonces:
app.Use(async (context, next) =>
{
var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
context.Items["CspNonce"] = nonce;
context.Response.Headers.Append(
"Content-Security-Policy",
$"default-src 'self'; script-src 'self' 'nonce-{nonce}'; style-src 'self' 'nonce-{nonce}'");
await next();
});
Rate Limiting
ASP.NET Core includes built-in rate limiting middleware (Microsoft.AspNetCore.RateLimiting, .NET 7+). Four algorithms are available: fixed window, sliding window, token bucket, and concurrency limiter.
Fixed Window
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", limiterOptions =>
{
limiterOptions.PermitLimit = 100;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueLimit = 0;
});
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
var app = builder.Build();
app.UseRateLimiter();
app.MapGet("/api/products", GetProducts)
.RequireRateLimiting("fixed");
Sliding Window
builder.Services.AddRateLimiter(options =>
{
options.AddSlidingWindowLimiter("sliding", limiterOptions =>
{
limiterOptions.PermitLimit = 100;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.SegmentsPerWindow = 6;
limiterOptions.QueueLimit = 0;
});
});
Token Bucket
builder.Services.AddRateLimiter(options =>
{
options.AddTokenBucketLimiter("token", limiterOptions =>
{
limiterOptions.TokenLimit = 100;
limiterOptions.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
limiterOptions.TokensPerPeriod = 10;
limiterOptions.QueueLimit = 0;
});
});
Concurrency Limiter
builder.Services.AddRateLimiter(options =>
{
options.AddConcurrencyLimiter("concurrent", limiterOptions =>
{
limiterOptions.PermitLimit = 10;
limiterOptions.QueueLimit = 5;
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
});
});
Per-User Rate Limiting
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("per-user", httpContext =>
{
var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)
?? httpContext.Connection.RemoteIpAddress?.ToString()
?? "anonymous";
return RateLimitPartition.GetFixedWindowLimiter(userId,
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 60,
Window = TimeSpan.FromMinutes(1)
});
});
});
Gotcha: UseRateLimiter() must be called after UseRouting() and before UseAuthorization() and endpoint mapping to apply correctly.
Agent Gotchas
- Do not use
AllowAnyOrigin() in production CORS policies -- always specify explicit origins. See [skill:dotnet-security-owasp] for CORS security implications.
- Do not forget
MapInboundClaims = false when using external OIDC providers -- without it, claim types are remapped to long XML namespace URIs, breaking role and name lookups.
- Do not hardcode JWT signing keys in source code or
appsettings.json -- use user secrets for development and environment variables or managed identity for production. See [skill:dotnet-secrets-management].
- Do not set
ClockSkew to TimeSpan.Zero -- small clock differences between token issuer and validator will cause spurious 401 errors. Use 1-2 minutes.
- Do not forget middleware order --
UseAuthentication() must come before UseAuthorization(), and UseCors() must come before UseAuthorization().
- Do not use
AllowAnyMethod() and AllowAnyHeader() together in production -- explicitly list allowed methods and headers to follow the principle of least privilege.
- Do not skip rate limiting on authentication endpoints --
/login and /register are common brute-force targets. Apply rate limiting to prevent credential stuffing.
- Do not use exception-driven rejection in auth paths -- use defensive parsing (
TryFromBase64String, length validation) on attacker-controlled input instead.
Prerequisites
- .NET 8.0+ (LTS baseline for Identity API endpoints, JWT bearer, CORS, rate limiting)
- .NET 10.0 for passkey/WebAuthn support
Microsoft.AspNetCore.Authentication.JwtBearer for JWT bearer authentication
Microsoft.AspNetCore.Authentication.OpenIdConnect for OIDC integration
Microsoft.AspNetCore.RateLimiting (included in shared framework .NET 7+)
References