Extract shared auth request validation

This commit is contained in:
2026-02-07 00:47:42 +01:00
parent b86343a59d
commit 16fcf4a432
2 changed files with 89 additions and 30 deletions

View File

@@ -15,33 +15,19 @@ public static class AuthEndpoints
group.MapPost("/register", async ([FromBody] RegisterRequest request, HttpContext ctx, AppDbContext db, IConfiguration config) => group.MapPost("/register", async ([FromBody] RegisterRequest request, HttpContext ctx, AppDbContext db, IConfiguration config) =>
{ {
var username = request.Username.Trim(); if (!AuthValidator.TryValidateRegistration(request, out var validated, out var registrationError))
if (string.IsNullOrWhiteSpace(username) || username.Length > 24) return Results.BadRequest(new { error = registrationError });
return Results.BadRequest(new { error = "Username is required and must be <= 24 characters." });
if (string.IsNullOrWhiteSpace(request.Password)) var exists = await db.Players.AnyAsync(p => p.NormalizedUsername == validated.NormalizedUsername);
return Results.BadRequest(new { error = "Password is required." });
if (request.DisplayName?.Trim().Length > 16)
return Results.BadRequest(new { error = "Display name must be <= 16 characters." });
var displayName = EndpointHelpers.TrimTo(request.DisplayName, 16);
if (string.IsNullOrWhiteSpace(displayName))
return Results.BadRequest(new { error = "Display name is required." });
var normalized = username.ToLowerInvariant();
var exists = await db.Players.AnyAsync(p => p.NormalizedUsername == normalized);
if (exists) if (exists)
return Results.Conflict(new { error = "Username already taken." }); return Results.Conflict(new { error = "Username already taken." });
var (hash, salt) = PasswordHasher.HashPassword(request.Password); var (hash, salt) = PasswordHasher.HashPassword(request.Password);
var adminKey = EndpointHelpers.TrimTo(request.AdminKey, 128);
var expectedAdminKey = config["ADMIN_PASSWORD"]; var expectedAdminKey = config["ADMIN_PASSWORD"];
var wantsAdmin = !string.IsNullOrWhiteSpace(adminKey); var wantsAdmin = !string.IsNullOrWhiteSpace(validated.AdminKey);
if (wantsAdmin) if (wantsAdmin)
{ {
if (string.IsNullOrWhiteSpace(expectedAdminKey) || adminKey != expectedAdminKey) if (string.IsNullOrWhiteSpace(expectedAdminKey) || validated.AdminKey != expectedAdminKey)
return Results.BadRequest(new { error = "Invalid admin key." }); return Results.BadRequest(new { error = "Invalid admin key." });
} }
@@ -50,11 +36,11 @@ public static class AuthEndpoints
var player = new Player var player = new Player
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Username = username, Username = validated.Username,
NormalizedUsername = normalized, NormalizedUsername = validated.NormalizedUsername,
PasswordHash = hash, PasswordHash = hash,
PasswordSalt = salt, PasswordSalt = salt,
DisplayName = displayName, DisplayName = validated.DisplayName,
IsAdmin = isAdmin, IsAdmin = isAdmin,
CreatedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow,
LastLoginAt = DateTimeOffset.UtcNow LastLoginAt = DateTimeOffset.UtcNow
@@ -76,20 +62,16 @@ public static class AuthEndpoints
group.MapPost("/login", async ([FromBody] LoginRequest request, HttpContext ctx, AppDbContext db) => group.MapPost("/login", async ([FromBody] LoginRequest request, HttpContext ctx, AppDbContext db) =>
{ {
var username = request.Username.Trim(); if (!AuthValidator.TryValidateLogin(request, out _, out var normalizedUsername, out var loginError))
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password)) return Results.BadRequest(new { error = loginError });
return Results.BadRequest(new { error = "Username and password are required." });
if (username.Length > 24)
return Results.BadRequest(new { error = "Username must be <= 24 characters." });
var normalized = username.ToLowerInvariant(); var player = await db.Players.FirstOrDefaultAsync(p => p.NormalizedUsername == normalizedUsername);
var player = await db.Players.FirstOrDefaultAsync(p => p.NormalizedUsername == normalized);
if (player == null || !PasswordHasher.Verify(request.Password, player.PasswordHash, player.PasswordSalt)) if (player == null || !PasswordHasher.Verify(request.Password, player.PasswordHash, player.PasswordSalt))
return Results.Json(new { error = "Invalid username or password." }, statusCode: StatusCodes.Status401Unauthorized); return Results.Json(new { error = "Invalid username or password." }, statusCode: StatusCodes.Status401Unauthorized);
if (string.IsNullOrWhiteSpace(player.DisplayName)) if (string.IsNullOrWhiteSpace(player.DisplayName))
{ {
player.DisplayName = EndpointHelpers.TrimTo(player.Username, 16); player.DisplayName = EndpointHelpers.TrimTo(player.Username, AuthValidator.MaxDisplayNameLength);
} }
player.LastLoginAt = DateTimeOffset.UtcNow; player.LastLoginAt = DateTimeOffset.UtcNow;

View File

@@ -0,0 +1,77 @@
using GameList.Contracts;
namespace GameList.Endpoints;
internal static class AuthValidator
{
public const int MaxUsernameLength = 24;
public const int MaxDisplayNameLength = 16;
public const int MaxAdminKeyLength = 128;
public static bool TryValidateRegistration(RegisterRequest request, out ValidatedRegistration validated, out string error)
{
var username = (request.Username ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(username) || username.Length > MaxUsernameLength)
{
validated = default;
error = $"Username is required and must be <= {MaxUsernameLength} characters.";
return false;
}
if (string.IsNullOrWhiteSpace(request.Password))
{
validated = default;
error = "Password is required.";
return false;
}
if ((request.DisplayName ?? string.Empty).Trim().Length > MaxDisplayNameLength)
{
validated = default;
error = $"Display name must be <= {MaxDisplayNameLength} characters.";
return false;
}
var displayName = EndpointHelpers.TrimTo(request.DisplayName, MaxDisplayNameLength);
if (string.IsNullOrWhiteSpace(displayName))
{
validated = default;
error = "Display name is required.";
return false;
}
var adminKey = EndpointHelpers.TrimTo(request.AdminKey, MaxAdminKeyLength);
validated = new ValidatedRegistration(username, username.ToLowerInvariant(), displayName, adminKey);
error = string.Empty;
return true;
}
public static bool TryValidateLogin(LoginRequest request, out string username, out string normalizedUsername, out string error)
{
username = (request.Username ?? string.Empty).Trim();
normalizedUsername = string.Empty;
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password))
{
error = "Username and password are required.";
return false;
}
if (username.Length > MaxUsernameLength)
{
error = $"Username must be <= {MaxUsernameLength} characters.";
return false;
}
normalizedUsername = username.ToLowerInvariant();
error = string.Empty;
return true;
}
public readonly record struct ValidatedRegistration(
string Username,
string NormalizedUsername,
string DisplayName,
string? AdminKey
);
}