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();
+ }
+}