1
0

217 lines
7.9 KiB
C#

using System.Security.Claims;
using System.Text;
using System.Text.Json.Serialization;
using Flawless.Communication.Response;
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();
SetupWebApplication(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<FormOptions>(opt =>
{
opt.MultipartBodyLengthLimit = long.MaxValue;
opt.ValueLengthLimit = int.MaxValue;
opt.MultipartHeadersLengthLimit = int.MaxValue;
});
// Api related
builder.Services.AddSingleton<PathTransformer>();
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<AppDbContext>(opt =>
{
opt.UseNpgsql(builder.Configuration.GetConnectionString("CoreDb"));
});
builder.Services.AddIdentityCore<AppUser>(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<AppDbContext>();
}
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<TokenGenerationService>();
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 SetupWebApplication(WebApplication app)
{
app.UseMiddleware<ExceptionTransformMiddleware>();
app.UseRouting();
// Config WebSocket support.
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(60),
KeepAliveTimeout = TimeSpan.FromSeconds(300),
});
// 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("/", () => "<p>Please use client app to open this server.</p>");
}
}
private static async Task OnTokenValidatedAsync(TokenValidatedContext context)
{
if (context.Principal != null)
{
var p = context.Principal!;
var auth = context.HttpContext.GetEndpoint()?.Metadata.GetOrderedMetadata<AuthorizeAttribute>();
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<UserManager<AppUser>>();
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."); //todo Fix lockout prob
}
// 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;
}
}