Update managing api security
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using AspNetCoreRateLimit;
|
||||
using HealthChecks.UI.Client;
|
||||
using Managing.Api.Authorization;
|
||||
using Managing.Api.Filters;
|
||||
@@ -15,6 +15,8 @@ using Managing.Infrastructure.Databases.PostgreSql;
|
||||
using Managing.Infrastructure.Databases.PostgreSql.Configurations;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
@@ -175,9 +177,64 @@ builder.Host.UseSerilog((hostBuilder, loggerConfiguration) =>
|
||||
builder.Services.AddOptions();
|
||||
builder.Services.Configure<PostgreSqlSettings>(builder.Configuration.GetSection(Constants.Databases.PostgreSql));
|
||||
builder.Services.Configure<InfluxDbSettings>(builder.Configuration.GetSection(Constants.Databases.InfluxDb));
|
||||
|
||||
// Configure Request Size Limits (protect against DoS attacks)
|
||||
builder.Services.Configure<FormOptions>(options =>
|
||||
{
|
||||
options.MultipartBodyLengthLimit = 10485760; // 10MB
|
||||
options.ValueLengthLimit = 10485760; // 10MB
|
||||
options.MultipartBoundaryLengthLimit = 256;
|
||||
options.KeyLengthLimit = 2048;
|
||||
});
|
||||
|
||||
builder.Services.Configure<KestrelServerOptions>(options =>
|
||||
{
|
||||
options.Limits.MaxRequestBodySize = 10485760; // 10MB
|
||||
options.Limits.MaxRequestHeadersTotalSize = 32768; // 32KB
|
||||
options.Limits.MaxConcurrentConnections = 100;
|
||||
options.Limits.MaxConcurrentUpgradedConnections = 100;
|
||||
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
|
||||
});
|
||||
|
||||
// Configure Rate Limiting
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.Configure<IpRateLimitOptions>(builder.Configuration.GetSection("IpRateLimiting"));
|
||||
builder.Services.Configure<IpRateLimitPolicies>(builder.Configuration.GetSection("IpRateLimitPolicies"));
|
||||
builder.Services.AddInMemoryRateLimiting();
|
||||
builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
|
||||
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
|
||||
|
||||
// Validate JWT secret configuration
|
||||
var jwtSecret = builder.Configuration["Jwt:Secret"];
|
||||
if (string.IsNullOrWhiteSpace(jwtSecret))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"JWT secret is not configured. Please set 'Jwt:Secret' in appsettings.json or environment variables.");
|
||||
}
|
||||
|
||||
if (jwtSecret.Length < 32)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"JWT secret must be at least 32 characters long. Current length: {jwtSecret.Length}. " +
|
||||
"This is a security requirement for production environments.");
|
||||
}
|
||||
|
||||
// Get issuer and audience configuration
|
||||
var validIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"];
|
||||
var validAudience = builder.Configuration["Authentication:Schemes:Bearer:ValidAudiences"];
|
||||
|
||||
// Determine if validation should be enabled (enable in production, allow override via config)
|
||||
var enableIssuerValidation = builder.Configuration.GetValue<bool>("Jwt:ValidateIssuer",
|
||||
!builder.Environment.IsDevelopment());
|
||||
var enableAudienceValidation = builder.Configuration.GetValue<bool>("Jwt:ValidateAudience",
|
||||
!builder.Environment.IsDevelopment());
|
||||
|
||||
// Configure clock skew (tolerance for time differences between servers)
|
||||
var clockSkewSeconds = builder.Configuration.GetValue<int>("Jwt:ClockSkewSeconds", 0);
|
||||
var clockSkew = TimeSpan.FromSeconds(clockSkewSeconds);
|
||||
|
||||
builder.Services
|
||||
.AddAuthentication(options =>
|
||||
{
|
||||
@@ -189,13 +246,16 @@ builder.Services
|
||||
o.SaveToken = true;
|
||||
o.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"],
|
||||
ValidAudience = builder.Configuration["Authentication:Schemes:Bearer:ValidAudiences"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey
|
||||
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"])),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true
|
||||
ValidIssuer = validIssuer,
|
||||
ValidAudience = validAudience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateIssuer = enableIssuerValidation && !string.IsNullOrWhiteSpace(validIssuer),
|
||||
ValidateAudience = enableAudienceValidation && !string.IsNullOrWhiteSpace(validAudience),
|
||||
ValidateLifetime = true, // Explicitly validate token expiration
|
||||
ClockSkew = clockSkew, // Configure clock skew tolerance
|
||||
RequireExpirationTime = true, // Ensure tokens have expiration
|
||||
RequireSignedTokens = true // Ensure tokens are signed
|
||||
};
|
||||
o.Events = new JwtBearerEvents
|
||||
{
|
||||
@@ -212,9 +272,20 @@ builder.Services
|
||||
},
|
||||
OnAuthenticationFailed = context =>
|
||||
{
|
||||
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
|
||||
var logger = context.HttpContext.RequestServices
|
||||
.GetService<ILogger<Program>>();
|
||||
|
||||
if (context.Exception is SecurityTokenExpiredException)
|
||||
{
|
||||
context.Response.Headers.Add("Token-Expired", "true");
|
||||
context.Response.Headers["Token-Expired"] = "true";
|
||||
logger?.LogWarning("JWT token expired for request: {Path}",
|
||||
context.Request.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger?.LogError(context.Exception,
|
||||
"JWT authentication failed for request: {Path}",
|
||||
context.Request.Path);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
@@ -222,19 +293,46 @@ builder.Services
|
||||
// --- IMPORTANT: Attach User to Context Here ---
|
||||
OnTokenValidated = async context =>
|
||||
{
|
||||
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
|
||||
// Assuming your JWT token contains a 'nameid' claim (or similar) for the user ID
|
||||
var userId = context.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
var logger = context.HttpContext.RequestServices
|
||||
.GetService<ILogger<Program>>();
|
||||
|
||||
try
|
||||
{
|
||||
// Fetch the full user object from your service
|
||||
var user = await userService.GetUserByAddressAsync(userId);
|
||||
if (user != null)
|
||||
var userService = context.HttpContext.RequestServices
|
||||
.GetRequiredService<IUserService>();
|
||||
|
||||
// JWT token contains 'address' claim (not NameIdentifier)
|
||||
var address = context.Principal.FindFirst("address")?.Value;
|
||||
|
||||
if (!string.IsNullOrEmpty(address))
|
||||
{
|
||||
// Attach the user object to HttpContext.Items
|
||||
context.HttpContext.Items["User"] = user;
|
||||
// Fetch the full user object from your service
|
||||
var user = await userService.GetUserByAddressAsync(address);
|
||||
if (user != null)
|
||||
{
|
||||
// Attach the user object to HttpContext.Items
|
||||
context.HttpContext.Items["User"] = user;
|
||||
logger?.LogDebug("User {Address} authenticated successfully", address);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger?.LogWarning(
|
||||
"JWT token validated but user not found for address: {Address}",
|
||||
address);
|
||||
context.Fail("User not found");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger?.LogWarning("JWT token validated but 'address' claim not found");
|
||||
context.Fail("Invalid token: missing address claim");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex,
|
||||
"Error during JWT token validation - user lookup failed");
|
||||
context.Fail("Authentication failed: user lookup error");
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
@@ -248,6 +346,7 @@ var allowedCorsOrigins = builder.Configuration
|
||||
.GetSection("Cors:AllowedOrigins")
|
||||
.Get<string[]>() ?? Array.Empty<string>();
|
||||
|
||||
// Configure CORS with improved security
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("CorsPolicy", policy =>
|
||||
@@ -256,9 +355,11 @@ builder.Services.AddCors(options =>
|
||||
{
|
||||
policy
|
||||
.WithOrigins(allowedCorsOrigins)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
.WithMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
||||
.WithHeaders("Content-Type", "Authorization", "X-Requested-With", "X-Correlation-ID")
|
||||
.WithExposedHeaders("Token-Expired", "X-Correlation-ID")
|
||||
.AllowCredentials()
|
||||
.SetPreflightMaxAge(TimeSpan.FromHours(24));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -331,6 +432,41 @@ builder.WebHost.SetupDiscordBot();
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
|
||||
// Rate Limiting - must be before authentication/authorization
|
||||
app.UseIpRateLimiting();
|
||||
|
||||
// Security Headers Middleware - add early in pipeline
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
// Security headers for all responses
|
||||
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
|
||||
context.Response.Headers.Append("X-Frame-Options", "DENY");
|
||||
context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
|
||||
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||
context.Response.Headers.Append("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
|
||||
|
||||
// Content Security Policy - only for non-Swagger endpoints
|
||||
if (!context.Request.Path.StartsWithSegments("/swagger") &&
|
||||
!context.Request.Path.StartsWithSegments("/health") &&
|
||||
!context.Request.Path.StartsWithSegments("/alive"))
|
||||
{
|
||||
context.Response.Headers.Append("Content-Security-Policy",
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;");
|
||||
}
|
||||
|
||||
// Remove server header (optional - Kestrel can be configured separately)
|
||||
context.Response.Headers.Remove("Server");
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
// HSTS - HTTP Strict Transport Security (production only)
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
if (enableSwagger)
|
||||
{
|
||||
app.UseOpenApi();
|
||||
|
||||
Reference in New Issue
Block a user