Extract shared auth request validation
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
77
Endpoints/AuthValidator.cs
Normal file
77
Endpoints/AuthValidator.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user