using System.Security.Claims; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.RateLimiting; using MemberCenter.Application.Abstractions; using MemberCenter.Application.Constants; using MemberCenter.Infrastructure.Configuration; using MemberCenter.Infrastructure.Identity; using MemberCenter.Infrastructure.Persistence; using MemberCenter.Infrastructure.Services; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; EnvLoader.LoadDotEnvIfDevelopment(); var builder = WebApplication.CreateBuilder(args); builder.Services.AddDataProtection() .SetApplicationName("MemberCenter"); builder.Services.AddDbContext(options => { var connectionString = builder.Configuration.GetConnectionString("Default") ?? Environment.GetEnvironmentVariable("ConnectionStrings__Default") ?? "Host=localhost;Database=member_center;Username=postgres;Password=postgres"; options.UseNpgsql(connectionString); options.UseOpenIddict(); }); builder.Services .AddIdentity(options => { options.User.RequireUniqueEmail = true; options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireUppercase = true; options.Password.RequireNonAlphanumeric = false; options.Password.RequiredLength = 8; options.Lockout.AllowedForNewUsers = true; options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15); }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); builder.Services.Configure(options => { options.ValidationInterval = TimeSpan.Zero; }); var googleClientId = builder.Configuration["Authentication:Google:ClientId"] ?? Environment.GetEnvironmentVariable("Authentication__Google__ClientId"); var googleClientSecret = builder.Configuration["Authentication:Google:ClientSecret"] ?? Environment.GetEnvironmentVariable("Authentication__Google__ClientSecret"); var authenticationBuilder = builder.Services.AddAuthentication(); if (!string.IsNullOrWhiteSpace(googleClientId) && !string.IsNullOrWhiteSpace(googleClientSecret)) { authenticationBuilder.AddGoogle(options => { options.ClientId = googleClientId; options.ClientSecret = googleClientSecret; }); } builder.Services.ConfigureApplicationCookie(options => { options.LoginPath = "/account/login"; var cookieDomain = builder.Configuration["Auth:CookieDomain"]; if (!string.IsNullOrWhiteSpace(cookieDomain)) { options.Cookie.Domain = cookieDomain; } options.Events = new CookieAuthenticationEvents { OnRedirectToLogin = context => HandleAdminAuthRedirectAsync(context), OnRedirectToAccessDenied = context => HandleAdminAuthRedirectAsync(context), OnValidatePrincipal = context => ValidatePrincipalAsync(context) }; }); builder.Services.AddAuthorization(options => { options.AddPolicy("Admin", policy => policy.RequireRole("admin", "superuser")); options.AddPolicy("Superuser", policy => policy.RequireRole("superuser")); }); builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.OnRejected = static async (context, token) => { if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) { context.HttpContext.Response.Headers.RetryAfter = Math.Ceiling(retryAfter.TotalSeconds).ToString(); } await context.HttpContext.Response.WriteAsync("Too many requests.", token); }; options.AddPolicy(RateLimitPolicyNames.PublicAuthLogin, context => CreateFixedWindowLimiter(context, "web-auth-login", permitLimit: 10, TimeSpan.FromMinutes(5))); options.AddPolicy(RateLimitPolicyNames.PublicAuthRegister, context => CreateFixedWindowLimiter(context, "web-auth-register", permitLimit: 5, TimeSpan.FromMinutes(15))); options.AddPolicy(RateLimitPolicyNames.PublicAuthRecovery, context => CreateFixedWindowLimiter(context, "web-auth-recovery", permitLimit: 5, TimeSpan.FromMinutes(15))); }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.Configure(builder.Configuration.GetSection("SendEngine")); builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddOpenIddict() .AddCore(options => { options.UseEntityFrameworkCore() .UseDbContext(); }); builder.Services.AddControllersWithViews(); builder.Services.AddHttpContextAccessor(); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseRouting(); app.UseRateLimiter(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllerRoute( name: "admin", pattern: "admin/{controller=Home}/{action=Index}/{id?}", defaults: new { area = "Admin" }); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run(); static Task HandleAdminAuthRedirectAsync(RedirectContext context) { if (context.Request.Path.StartsWithSegments("/admin", StringComparison.OrdinalIgnoreCase)) { context.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; } context.Response.Redirect(context.RedirectUri); return Task.CompletedTask; } static async Task ValidatePrincipalAsync(CookieValidatePrincipalContext context) { var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier); if (!Guid.TryParse(userId, out var parsedUserId)) { return; } var userManager = context.HttpContext.RequestServices.GetRequiredService>(); var user = await userManager.FindByIdAsync(parsedUserId.ToString()); if (user is null || user.DisabledAt.HasValue) { context.RejectPrincipal(); await context.HttpContext.SignOutAsync(); } } static RateLimitPartition CreateFixedWindowLimiter( HttpContext context, string policyPrefix, int permitLimit, TimeSpan window) { var identifier = context.User.Identity?.IsAuthenticated == true ? context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? context.User.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString() ?? "unknown" : context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; var partitionKey = $"{policyPrefix}:{identifier}"; return RateLimitPartition.GetFixedWindowLimiter(partitionKey, _ => new FixedWindowRateLimiterOptions { PermitLimit = permitLimit, Window = window, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 0, AutoReplenishment = true }); }