diff --git a/apps/server/AliasVault.Admin/Main/Pages/Users/View/Index.razor b/apps/server/AliasVault.Admin/Main/Pages/Users/View/Index.razor index b070456c6..e7a9317ca 100644 --- a/apps/server/AliasVault.Admin/Main/Pages/Users/View/Index.razor +++ b/apps/server/AliasVault.Admin/Main/Pages/Users/View/Index.razor @@ -2,6 +2,7 @@ @using AliasVault.Admin.Main.Pages.Users.View.Components @using AliasVault.Admin.Main.Models @using AliasVault.Admin.Services +@using AliasVault.Auth @using Microsoft.EntityFrameworkCore @inherits MainBase @inject StatisticsService StatisticsService @@ -642,6 +643,9 @@ Do you want to proceed with the restoration?")) { /// private async Task ChangeUsername() { + // Normalize to lowercase and trim to match how usernames are stored at registration. + NewUsername = UsernameHelper.NormalizeUsername(NewUsername); + if (string.IsNullOrWhiteSpace(NewUsername)) { UsernameValidationError = "Username cannot be empty."; diff --git a/apps/server/AliasVault.Api/Controllers/AuthController.cs b/apps/server/AliasVault.Api/Controllers/AuthController.cs index 5f51b1a1e..dd4111951 100644 --- a/apps/server/AliasVault.Api/Controllers/AuthController.cs +++ b/apps/server/AliasVault.Api/Controllers/AuthController.cs @@ -458,7 +458,7 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM var user = new AliasVaultUser { - UserName = model.Username, + UserName = UsernameHelper.NormalizeUsername(model.Username), SrpIdentity = srpIdentity, CreatedAt = timeProvider.UtcNow, UpdatedAt = timeProvider.UtcNow, @@ -551,7 +551,7 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM return BadRequest(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.USERNAME_REQUIRED, 400)); } - var normalizedUsername = NormalizeUsername(model.Username); + var normalizedUsername = UsernameHelper.NormalizeUsername(model.Username); var existingUser = await userManager.FindByNameAsync(normalizedUsername); if (existingUser != null) @@ -586,7 +586,7 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM } // Verify the username matches the current user. - if (user.UserName != model.Username) + if (!string.Equals(user.UserName, model.Username, StringComparison.OrdinalIgnoreCase)) { return BadRequest(ApiErrorCodeHelper.CreateValidationErrorResponse(ApiErrorCode.USERNAME_MISMATCH, 400)); } @@ -826,7 +826,7 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM } // Verify the username matches the current user. - if (user.UserName != model.Username) + if (!string.Equals(user.UserName, model.Username, StringComparison.OrdinalIgnoreCase)) { return BadRequest(ApiErrorCodeHelper.CreateValidationErrorResponse(ApiErrorCode.USERNAME_MISMATCH, 400)); } @@ -850,16 +850,6 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM return Ok(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.ACCOUNT_SUCCESSFULLY_DELETED, 200)); } - /// - /// Normalizes a username by trimming and lowercasing it. - /// - /// The username to normalize. - /// The normalized username. - private static string NormalizeUsername(string username) - { - return username.ToLowerInvariant().Trim(); - } - /// /// Generate a refresh token for a user. This token is used to request a new access token when the current /// access token expires. The refresh token is long-lived by design. diff --git a/apps/server/AliasVault.Api/Controllers/VaultController.cs b/apps/server/AliasVault.Api/Controllers/VaultController.cs index a36cd63a6..1da925eaa 100644 --- a/apps/server/AliasVault.Api/Controllers/VaultController.cs +++ b/apps/server/AliasVault.Api/Controllers/VaultController.cs @@ -145,7 +145,7 @@ public class VaultController(ILogger logger, IAliasServerDbCont // If they do not match reject the request. This is important because it's // possible that a user has logged in with a different username than the one // that is being used to update the vault (e.g. if working with multiple tabs). - if (user.UserName != model.Username) + if (!string.Equals(user.UserName, model.Username, StringComparison.OrdinalIgnoreCase)) { return BadRequest(ApiErrorCodeHelper.CreateValidationErrorResponse(ApiErrorCode.USERNAME_MISMATCH, 400)); } @@ -233,7 +233,7 @@ public class VaultController(ILogger logger, IAliasServerDbCont // If they do not match reject the request. This is important because it's // possible that a user has logged in with a different username than the one // that is being used to update the vault (e.g. if working with multiple tabs). - if (model.Username != user.UserName) + if (!string.Equals(user.UserName, model.Username, StringComparison.OrdinalIgnoreCase)) { return BadRequest(ApiErrorCodeHelper.CreateValidationErrorResponse(ApiErrorCode.USERNAME_MISMATCH, 400)); } diff --git a/apps/server/Utilities/AliasVault.Auth/UsernameHelper.cs b/apps/server/Utilities/AliasVault.Auth/UsernameHelper.cs new file mode 100644 index 000000000..101d3f492 --- /dev/null +++ b/apps/server/Utilities/AliasVault.Auth/UsernameHelper.cs @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) aliasvault. All rights reserved. +// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.Auth; + +/// +/// Helper for normalizing usernames consistently across the server. +/// +public static class UsernameHelper +{ + /// + /// Normalizes a username by lowercasing and trimming it. + /// Used by all code paths that store a username. + /// + /// The username to normalize. + /// The normalized username. + public static string NormalizeUsername(string username) + { + return username.ToLowerInvariant().Trim(); + } +}