Merge authentication into Questions API

This commit is contained in:
hz
2026-07-20 17:21:15 -05:00
parent 2dd4fa6420
commit 5c406a271f
24 changed files with 1472 additions and 30 deletions
+69 -4
View File
@@ -1,13 +1,24 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using QuestionsApi.Services;
using MongoDB.Bson;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.ForwardLimit = 2;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
builder.Services.AddSingleton<MongoDatabaseProvider>();
builder.Services.AddSingleton<QuestionStore>();
@@ -26,10 +37,43 @@ builder.Services.AddSingleton<QuestionBookmarkStore>();
builder.Services.AddSingleton<ReviewQueueStore>();
builder.Services.AddSingleton<DashboardStore>();
builder.Services.AddSingleton<UserProfileStore>();
builder.Services.AddSingleton<AuthUserStore>();
builder.Services.AddSingleton<TokenBlacklistStore>();
builder.Services.AddSingleton<JwtTokenService>();
builder.Services.AddSingleton<LegalPolicy>();
builder.Services.AddSingleton<SteamAuthTransactionStore>();
builder.Services.AddSingleton<SteamOpenIdService>();
builder.Services.AddSingleton<PasswordResetStore>();
builder.Services.AddSingleton<PasswordResetCodeService>();
builder.Services.AddSingleton<IPasswordResetEmailSender, SmtpPasswordResetEmailSender>();
builder.Services.AddSingleton<AuthDataInitializer>();
builder.Services.AddSingleton<DonationStore>();
builder.Services.AddSingleton<IStripeDonationGateway, StripeDonationGateway>();
builder.Services.AddSingleton<ServiceMetrics>();
builder.Services.AddHostedService<AnswerStatisticsMaintenanceService>();
builder.Services.AddHttpClient();
builder.Services.AddRateLimiter(options =>
{
static RateLimitPartition<string> Fixed(HttpContext context, int permits, TimeSpan window) =>
RateLimitPartition.GetFixedWindowLimiter(
context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = permits,
Window = window,
QueueLimit = 0,
AutoReplenishment = true
});
options.AddPolicy("auth-register", context => Fixed(context, 5, TimeSpan.FromMinutes(15)));
options.AddPolicy("auth-login", context => Fixed(context, 20, TimeSpan.FromMinutes(5)));
options.AddPolicy("auth-refresh", context => Fixed(context, 60, TimeSpan.FromMinutes(5)));
options.AddPolicy("password-reset-request", context => Fixed(context, 5, TimeSpan.FromMinutes(15)));
options.AddPolicy("password-reset-confirm", context => Fixed(context, 10, TimeSpan.FromMinutes(15)));
options.AddPolicy("steam-start", context => Fixed(context, 10, TimeSpan.FromMinutes(1)));
options.AddPolicy("steam-poll", context => Fixed(context, 120, TimeSpan.FromMinutes(1)));
options.AddPolicy("steam-complete", context => Fixed(context, 20, TimeSpan.FromMinutes(1)));
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
@@ -78,12 +122,30 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
{
OnTokenValidated = async context =>
{
var userId = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrWhiteSpace(userId))
var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrWhiteSpace(userId))
{
var profiles = context.HttpContext.RequestServices.GetRequiredService<UserProfileStore>();
if (await profiles.IsDeletedAsync(userId)) context.Fail("Account deleted");
context.Fail("User identity missing");
return;
}
var services = context.HttpContext.RequestServices;
var userTask = services.GetRequiredService<AuthUserStore>().GetByIdAsync(userId);
var deletedTask = services.GetRequiredService<UserProfileStore>().IsDeletedAsync(userId);
var jti = context.Principal?.FindFirstValue(JwtRegisteredClaimNames.Jti);
var revokedTask = string.IsNullOrWhiteSpace(jti)
? Task.FromResult(false)
: services.GetRequiredService<TokenBlacklistStore>().ContainsAsync(jti);
await Task.WhenAll(userTask, deletedTask, revokedTask);
var user = await userTask;
var tokenVersion = int.TryParse(context.Principal?.FindFirstValue("auth_ver"), out var parsedVersion)
? parsedVersion
: 0;
if (user is null || user.AuthVersion != tokenVersion)
context.Fail("Session revoked");
else if (await deletedTask)
context.Fail("Account deleted");
else if (await revokedTask)
context.Fail("Token revoked");
}
};
});
@@ -91,7 +153,9 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
builder.Services.AddAuthorization();
var app = builder.Build();
await app.Services.GetRequiredService<AuthDataInitializer>().InitializeAsync();
app.UseForwardedHeaders();
app.Use(async (context, next) =>
{
var started = System.Diagnostics.Stopwatch.StartNew();
@@ -152,6 +216,7 @@ app.UseSwaggerUI(o =>
o.SwaggerEndpoint("/swagger/v1/swagger.json", "Questions API v1");
o.RoutePrefix = "swagger";
});
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();