217 lines
7.9 KiB
C#
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;
|
|
}
|
|
} |