using System.Security; using System.Security.Claims; using System.Text; using Flawless.Server.Middlewares; using Flawless.Server.Models; using Flawless.Server.Services; using Flawless.Server.Utility; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; namespace Flawless.Server; public static class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); ConfigAppService(builder); ConfigAuthentication(builder); ConfigDbContext(builder); var app = builder.Build(); SetupMiddleware(app); app.Run(); } private static void ConfigAppService(WebApplicationBuilder builder) { // Set size limit builder.WebHost.ConfigureKestrel(opt => { opt.Limits.MaxRequestBodySize = long.MaxValue; // As big as possible... opt.Limits.MaxRequestHeaderCount = int.MaxValue; // As big as possible... }); builder.Services.Configure(opt => { opt.MultipartBodyLengthLimit = long.MaxValue; opt.ValueLengthLimit = int.MaxValue; opt.MultipartHeadersLengthLimit = int.MaxValue; }); // Logic services builder.Services.AddHttpClient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Api related builder.Services.AddOpenApi(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(opt => { opt.DocInclusionPredicate((name, api) => api.HttpMethod != null); // Filter out WebSocket methods opt.SupportNonNullableReferenceTypes(); opt.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "JWT Authorization header using the Bearer scheme.", Name = "Authorization", Type = SecuritySchemeType.ApiKey, }); opt.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, [] } }); }); } private static void ConfigDbContext(WebApplicationBuilder builder) { // Data connection related. builder.Services.AddDbContext(opt => { opt.UseNpgsql(builder.Configuration.GetConnectionString("CoreDb")); }); builder.Logging.ClearProviders() .AddConsole() .AddDebug() .Services.AddSingleton(); builder.Services.AddIdentityCore(opt => { opt.Password.RequireLowercase = false; opt.Password.RequireUppercase = false; opt.Password.RequireNonAlphanumeric = false; opt.Password.RequireDigit = false; opt.Password.RequiredLength = 6; opt.User.RequireUniqueEmail = true; opt.SignIn.RequireConfirmedAccount = false; opt.SignIn.RequireConfirmedEmail = false; opt.SignIn.RequireConfirmedPhoneNumber = false; opt.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier; opt.ClaimsIdentity.SecurityStampClaimType = FlawlessClaimsType.SecurityStamp; }) .AddSignInManager() .AddEntityFrameworkStores(); } private static void ConfigAuthentication(WebApplicationBuilder builder) { var config = builder.Configuration; var secretKey = config["Jwt:SecretKey"] ?? throw new ApplicationException("No secret key found."); var issuer = config["Jwt:Issuer"] ?? throw new ApplicationException("No issuer found."); // Authentication related. builder.Services.AddSingleton(); builder.Services.AddAuthentication(opt => { opt.DefaultScheme = opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(opt => { opt.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = false, ValidateIssuerSigningKey = true, ValidIssuer = issuer, ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)), ClockSkew = TimeSpan.Zero, RoleClaimType = ClaimTypes.Role, NameClaimType = ClaimTypes.Name, }; opt.Events = new JwtBearerEvents { OnAuthenticationFailed = OnAuthenticationFailedAsync, OnTokenValidated = OnTokenValidatedAsync }; }); } private static void SetupMiddleware(WebApplication app) { app.UseMiddleware(); app.UseMiddleware(); app.UseRouting(); // Configure identity control app.UseAuthentication(); app.UseAuthorization(); // Configure actual controllers app.MapControllers(); // Configure fallback endpoints if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.UseSwagger(); app.UseSwaggerUI(); app.MapGet("/", () => Results.Redirect("/swagger/index.html")); } else { app.MapGet("/", () => "

Please use client app to open this server.

"); } } private static async Task OnTokenValidatedAsync(TokenValidatedContext context) { if (context.Principal != null) { var auth = context.HttpContext.GetEndpoint()?.Metadata.GetOrderedMetadata(); var p = context.Principal!; if (auth?.Any() ?? false) { var id = p.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (id == null) throw new SecurityTokenExpiredException("User is not defined in the token!"); var stamp = p.FindFirst(FlawlessClaimsType.SecurityStamp)?.Value; if (stamp == null) throw new SecurityTokenExpiredException("No valid SecurityStamp found."); // Validate user status var db = context.HttpContext.RequestServices.GetRequiredService>(); var u = await db.FindByIdAsync(id!); if (u == null) throw new SecurityTokenExpiredException("User is not existed."); if (u.SecurityStamp != stamp) throw new SecurityTokenExpiredException("SecurityStamp is mismatched."); if (u.LockoutEnabled) throw new SecurityTokenExpiredException("User has been locked."); } // Extract user info into HttpContext context.HttpContext.User = p; context.Success(); } } private static Task OnAuthenticationFailedAsync(AuthenticationFailedContext context) { if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.Response.Headers.Append("Token-Expired", "true"); } return Task.CompletedTask; } }