From 65342a2a8d528c8c00827550b2fce595737cdaae Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Mon, 19 Aug 2024 23:33:50 +0200 Subject: [PATCH] Change email to username for main user authentication (#174) --- src/AliasVault.Admin/AliasVault.Admin.csproj | 1 + .../Main/Pages/Users/Users.razor | 5 ++- .../Controllers/AuthController.cs | 33 +++++++++---------- .../Auth/Pages/Base/LoginBase.cs | 15 +++++---- src/AliasVault.Client/Auth/Pages/Login.razor | 8 ++--- .../Auth/Pages/Register.razor | 8 ++--- src/AliasVault.Shared/Models/LoginModel.cs | 5 ++- src/AliasVault.Shared/Models/RegisterModel.cs | 5 ++- .../Models/WebApi/Auth/LoginRequest.cs | 10 +++--- .../WebApi/Auth/ValidateLoginRequest.cs | 18 +++++----- .../Cryptography/Models/SrpSignup.cs | 22 ++++++------- src/Utilities/Cryptography/Srp.cs | 26 +++++++-------- 12 files changed, 78 insertions(+), 78 deletions(-) diff --git a/src/AliasVault.Admin/AliasVault.Admin.csproj b/src/AliasVault.Admin/AliasVault.Admin.csproj index 53ab5e543..f0dcb4655 100644 --- a/src/AliasVault.Admin/AliasVault.Admin.csproj +++ b/src/AliasVault.Admin/AliasVault.Admin.csproj @@ -6,6 +6,7 @@ enable aspnet-AliasVault.Admin-1DAADE35-C01B-43BB-B440-AA5E1E0B672D Linux + 1701;1702;NU1900 diff --git a/src/AliasVault.Admin/Main/Pages/Users/Users.razor b/src/AliasVault.Admin/Main/Pages/Users/Users.razor index f77e8e57c..642ab8a51 100644 --- a/src/AliasVault.Admin/Main/Pages/Users/Users.razor +++ b/src/AliasVault.Admin/Main/Pages/Users/Users.razor @@ -105,8 +105,7 @@ else if (SearchTerm.Length > 0) { - query = query.Where(x => EF.Functions.Like(x.UserName!.ToLower(), "%" + SearchTerm.ToLower() + "%") || - EF.Functions.Like(x.Email!.ToLower(), "%" + SearchTerm.ToLower() + "%")); + query = query.Where(x => EF.Functions.Like(x.UserName!.ToLower(), "%" + SearchTerm.ToLower() + "%")); } TotalRecords = await query.CountAsync(); @@ -140,7 +139,7 @@ else VaultCount = user.Vaults.Count(), EmailClaimCount = user.EmailClaims.Count(), VaultStorageInKb = user.Vaults.Sum(x => x.FileSize), - LastVaultUpdate = user.Vaults.Max(x => x.CreatedAt), + LastVaultUpdate = user.Vaults.Any() ? user.Vaults.Max(x => x.CreatedAt) : user.CreatedAt, }).ToList(); IsLoading = false; diff --git a/src/AliasVault.Api/Controllers/AuthController.cs b/src/AliasVault.Api/Controllers/AuthController.cs index eaed6eaea..f341e88ac 100644 --- a/src/AliasVault.Api/Controllers/AuthController.cs +++ b/src/AliasVault.Api/Controllers/AuthController.cs @@ -39,9 +39,9 @@ using Microsoft.IdentityModel.Tokens; public class AuthController(IDbContextFactory dbContextFactory, UserManager userManager, SignInManager signInManager, IConfiguration configuration, IMemoryCache cache, ITimeProvider timeProvider) : ControllerBase { /// - /// Error message for invalid email or password. + /// Error message for invalid username or password. /// - public static readonly string[] InvalidEmailOrPasswordError = ["Invalid email or password. Please try again."]; + public static readonly string[] InvalidUsernameOrPasswordError = ["Invalid username or password. Please try again."]; /// /// Login endpoint used to process login attempt using credentials. @@ -51,17 +51,17 @@ public class AuthController(IDbContextFactory dbContextFac [HttpPost("login")] public async Task Login([FromBody] LoginRequest model) { - var user = await userManager.FindByEmailAsync(model.Email); + var user = await userManager.FindByNameAsync(model.Username); if (user == null) { - return BadRequest(ServerValidationErrorResponse.Create(InvalidEmailOrPasswordError, 400)); + return BadRequest(ServerValidationErrorResponse.Create(InvalidUsernameOrPasswordError, 400)); } // Server creates ephemeral and sends to client var ephemeral = Cryptography.Srp.GenerateEphemeralServer(user.Verifier); // Store the server ephemeral in memory cache for Validate() endpoint to use. - cache.Set(model.Email, ephemeral.Secret, TimeSpan.FromMinutes(5)); + cache.Set(model.Username, ephemeral.Secret, TimeSpan.FromMinutes(5)); return Ok(new LoginResponse(user.Salt, ephemeral.Public)); } @@ -74,15 +74,15 @@ public class AuthController(IDbContextFactory dbContextFac [HttpPost("validate")] public async Task Validate([FromBody] ValidateLoginRequest model) { - var user = await userManager.FindByEmailAsync(model.Email); + var user = await userManager.FindByNameAsync(model.Username); if (user == null) { - return BadRequest(ServerValidationErrorResponse.Create(InvalidEmailOrPasswordError, 400)); + return BadRequest(ServerValidationErrorResponse.Create(InvalidUsernameOrPasswordError, 400)); } - if (!cache.TryGetValue(model.Email, out var serverSecretEphemeral) || serverSecretEphemeral is not string) + if (!cache.TryGetValue(model.Username, out var serverSecretEphemeral) || serverSecretEphemeral is not string) { - return BadRequest(ServerValidationErrorResponse.Create(InvalidEmailOrPasswordError, 400)); + return BadRequest(ServerValidationErrorResponse.Create(InvalidUsernameOrPasswordError, 400)); } try @@ -91,7 +91,7 @@ public class AuthController(IDbContextFactory dbContextFac serverSecretEphemeral.ToString() ?? string.Empty, model.ClientPublicEphemeral, user.Salt, - model.Email, + model.Username, user.Verifier, model.ClientSessionProof); @@ -103,7 +103,7 @@ public class AuthController(IDbContextFactory dbContextFac } catch { - return BadRequest(ServerValidationErrorResponse.Create(InvalidEmailOrPasswordError, 400)); + return BadRequest(ServerValidationErrorResponse.Create(InvalidUsernameOrPasswordError, 400)); } } @@ -120,13 +120,13 @@ public class AuthController(IDbContextFactory dbContextFac var principal = GetPrincipalFromExpiredToken(tokenModel.Token); if (principal.FindFirst(ClaimTypes.NameIdentifier)?.Value == null) { - return Unauthorized("User not found (email-1)"); + return Unauthorized("User not found (name-1)"); } var user = await userManager.FindByIdAsync(principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty); if (user == null) { - return Unauthorized("User not found (email-2)"); + return Unauthorized("User not found (name-2)"); } // Check if the refresh token is valid. @@ -172,13 +172,13 @@ public class AuthController(IDbContextFactory dbContextFac var principal = GetPrincipalFromExpiredToken(model.Token); if (principal.FindFirst(ClaimTypes.NameIdentifier)?.Value == null) { - return Unauthorized("User not found (email-1)"); + return Unauthorized("User not found (name-1)"); } var user = await userManager.FindByIdAsync(principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty); if (user == null) { - return Unauthorized("User not found (email-2)"); + return Unauthorized("User not found (name-2)"); } // Check if the refresh token is valid. @@ -204,7 +204,7 @@ public class AuthController(IDbContextFactory dbContextFac [HttpPost("register")] public async Task Register([FromBody] SrpSignup model) { - var user = new AliasVaultUser { UserName = model.Email, Email = model.Email, Salt = model.Salt, Verifier = model.Verifier, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; + var user = new AliasVaultUser { UserName = model.Username, Salt = model.Salt, Verifier = model.Verifier, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; var result = await userManager.CreateAsync(user); if (result.Succeeded) @@ -310,7 +310,6 @@ public class AuthController(IDbContextFactory dbContextFac { new(ClaimTypes.NameIdentifier, user.Id), new(ClaimTypes.Name, user.UserName ?? string.Empty), - new(ClaimTypes.Email, user.Email ?? string.Empty), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; diff --git a/src/AliasVault.Client/Auth/Pages/Base/LoginBase.cs b/src/AliasVault.Client/Auth/Pages/Base/LoginBase.cs index 9643175a4..670b9ab53 100644 --- a/src/AliasVault.Client/Auth/Pages/Base/LoginBase.cs +++ b/src/AliasVault.Client/Auth/Pages/Base/LoginBase.cs @@ -97,13 +97,16 @@ public class LoginBase : OwningComponentBase /// /// Gets the username from the authentication state asynchronously. /// - /// Email address. + /// Username. /// Password. /// List of errors if something went wrong. - protected async Task> ProcessLoginAsync(string email, string password) + protected async Task> ProcessLoginAsync(string username, string password) { + // Sanitize username + username = username.ToLowerInvariant().Trim(); + // Send request to server with email to get server ephemeral public key. - var result = await Http.PostAsJsonAsync("api/v1/Auth/login", new LoginRequest(email)); + var result = await Http.PostAsJsonAsync("api/v1/Auth/login", new LoginRequest(username)); var responseContent = await result.Content.ReadAsStringAsync(); if (!result.IsSuccessStatusCode) @@ -125,16 +128,16 @@ public class LoginBase : OwningComponentBase var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty); var clientEphemeral = Srp.GenerateEphemeralClient(); - var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, email, passwordHashString); + var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, username, passwordHashString); var clientSession = Srp.DeriveSessionClient( privateKey, clientEphemeral.Secret, loginResponse.ServerEphemeral, loginResponse.Salt, - email); + username); // 4. Client sends proof of session key to server. - result = await Http.PostAsJsonAsync("api/v1/Auth/validate", new ValidateLoginRequest(email, clientEphemeral.Public, clientSession.Proof)); + result = await Http.PostAsJsonAsync("api/v1/Auth/validate", new ValidateLoginRequest(username, clientEphemeral.Public, clientSession.Proof)); responseContent = await result.Content.ReadAsStringAsync(); if (!result.IsSuccessStatusCode) diff --git a/src/AliasVault.Client/Auth/Pages/Login.razor b/src/AliasVault.Client/Auth/Pages/Login.razor index 32983110f..5fa178136 100644 --- a/src/AliasVault.Client/Auth/Pages/Login.razor +++ b/src/AliasVault.Client/Auth/Pages/Login.razor @@ -15,9 +15,9 @@
- - - + + +
@@ -64,7 +64,7 @@ try { - var errors = await ProcessLoginAsync(LoginModel.Email, LoginModel.Password); + var errors = await ProcessLoginAsync(LoginModel.Username, LoginModel.Password); foreach (var error in errors) { ServerValidationErrors.AddError(error); diff --git a/src/AliasVault.Client/Auth/Pages/Register.razor b/src/AliasVault.Client/Auth/Pages/Register.razor index b24c2636c..f1f454254 100644 --- a/src/AliasVault.Client/Auth/Pages/Register.razor +++ b/src/AliasVault.Client/Auth/Pages/Register.razor @@ -22,9 +22,9 @@
- - - + + +
@@ -71,7 +71,7 @@ byte[] passwordHash = await Encryption.DeriveKeyFromPasswordAsync(RegisterModel.Password, salt); var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty); - var srpSignup = Cryptography.Srp.SignupPrepareAsync(client, salt, RegisterModel.Email, passwordHashString); + var srpSignup = Cryptography.Srp.SignupPrepareAsync(client, salt, RegisterModel.Username, passwordHashString); var result = await Http.PostAsJsonAsync("api/v1/Auth/register", srpSignup); var responseContent = await result.Content.ReadAsStringAsync(); diff --git a/src/AliasVault.Shared/Models/LoginModel.cs b/src/AliasVault.Shared/Models/LoginModel.cs index e39e40c04..5be4e71da 100644 --- a/src/AliasVault.Shared/Models/LoginModel.cs +++ b/src/AliasVault.Shared/Models/LoginModel.cs @@ -15,11 +15,10 @@ using System.ComponentModel.DataAnnotations; public class LoginModel { /// - /// Gets or sets the email. + /// Gets or sets the username. /// [Required] - [EmailAddress] - public string Email { get; set; } = null!; + public string Username { get; set; } = null!; /// /// Gets or sets the password. diff --git a/src/AliasVault.Shared/Models/RegisterModel.cs b/src/AliasVault.Shared/Models/RegisterModel.cs index 42c0dfd95..f6eb8160a 100644 --- a/src/AliasVault.Shared/Models/RegisterModel.cs +++ b/src/AliasVault.Shared/Models/RegisterModel.cs @@ -16,11 +16,10 @@ using AliasVault.Shared.Models.Validation; public class RegisterModel { /// - /// Gets or sets the email. + /// Gets or sets the username. /// [Required] - [EmailAddress] - public string Email { get; set; } = null!; + public string Username { get; set; } = null!; /// /// Gets or sets the password. diff --git a/src/AliasVault.Shared/Models/WebApi/Auth/LoginRequest.cs b/src/AliasVault.Shared/Models/WebApi/Auth/LoginRequest.cs index 1243a8167..6b15bbc77 100644 --- a/src/AliasVault.Shared/Models/WebApi/Auth/LoginRequest.cs +++ b/src/AliasVault.Shared/Models/WebApi/Auth/LoginRequest.cs @@ -15,14 +15,14 @@ public class LoginRequest /// /// Initializes a new instance of the class. /// - /// Email. - public LoginRequest(string email) + /// Username. + public LoginRequest(string username) { - Email = email; + Username = username.ToLowerInvariant().Trim(); } /// - /// Gets or sets the email address. + /// Gets the username. /// - public string Email { get; set; } + public string Username { get; } } diff --git a/src/AliasVault.Shared/Models/WebApi/Auth/ValidateLoginRequest.cs b/src/AliasVault.Shared/Models/WebApi/Auth/ValidateLoginRequest.cs index ea5d391dd..88e4b9971 100644 --- a/src/AliasVault.Shared/Models/WebApi/Auth/ValidateLoginRequest.cs +++ b/src/AliasVault.Shared/Models/WebApi/Auth/ValidateLoginRequest.cs @@ -15,29 +15,29 @@ namespace AliasVault.Shared.Models.WebApi.Auth /// /// Initializes a new instance of the class. /// - /// Email. + /// Username. /// Client public ephemeral. /// Client session proof. - public ValidateLoginRequest(string email, string clientPublicEphemeral, string clientSessionProof) + public ValidateLoginRequest(string username, string clientPublicEphemeral, string clientSessionProof) { - Email = email; + Username = username.ToLowerInvariant().Trim(); ClientPublicEphemeral = clientPublicEphemeral; ClientSessionProof = clientSessionProof; } /// - /// Gets or sets the email. + /// Gets the username. /// - public string Email { get; set; } + public string Username { get; } /// - /// Gets or sets the client's public ephemeral value. + /// Gets the client's public ephemeral value. /// - public string ClientPublicEphemeral { get; set; } + public string ClientPublicEphemeral { get; } /// - /// Gets or sets the client's session proof. + /// Gets the client's session proof. /// - public string ClientSessionProof { get; set; } + public string ClientSessionProof { get; } } } diff --git a/src/Utilities/Cryptography/Models/SrpSignup.cs b/src/Utilities/Cryptography/Models/SrpSignup.cs index 68e867c2f..954fb3cf8 100644 --- a/src/Utilities/Cryptography/Models/SrpSignup.cs +++ b/src/Utilities/Cryptography/Models/SrpSignup.cs @@ -15,35 +15,35 @@ public class SrpSignup /// /// Initializes a new instance of the class with the specified salt, private key, and verifier. /// - /// The email address. + /// The username. /// The salt value. /// The private key value. /// The verifier value. - public SrpSignup(string email, string salt, string privateKey, string verifier) + public SrpSignup(string username, string salt, string privateKey, string verifier) { - Email = email; + Username = username.ToLowerInvariant().Trim(); Salt = salt; PrivateKey = privateKey; Verifier = verifier; } /// - /// Gets or sets the email value. + /// Gets the username value. /// - public string Email { get; set; } + public string Username { get; } /// - /// Gets or sets the salt value. + /// Gets the salt value. /// - public string Salt { get; set; } + public string Salt { get; } /// - /// Gets or sets the private key value. + /// Gets the private key value. /// - public string PrivateKey { get; set; } + public string PrivateKey { get; } /// - /// Gets or sets the verifier value. + /// Gets the verifier value. /// - public string Verifier { get; set; } + public string Verifier { get; } } diff --git a/src/Utilities/Cryptography/Srp.cs b/src/Utilities/Cryptography/Srp.cs index af164f5eb..2c2258605 100644 --- a/src/Utilities/Cryptography/Srp.cs +++ b/src/Utilities/Cryptography/Srp.cs @@ -21,31 +21,31 @@ public static class Srp /// /// SrpClient. /// Salt. - /// Email. + /// Username. /// Hashed password string. /// SrpSignup model. - public static SrpSignup SignupPrepareAsync(SrpClient client, string salt, string email, string passwordHashString) + public static SrpSignup SignupPrepareAsync(SrpClient client, string salt, string username, string passwordHashString) { // Derive a key from the password using Argon2id // Signup: client generates a salt and verifier. - var privateKey = DerivePrivateKey(salt, email, passwordHashString); + var privateKey = DerivePrivateKey(salt, username, passwordHashString); var verifier = client.DeriveVerifier(privateKey); - return new SrpSignup(email, salt, privateKey, verifier); + return new SrpSignup(username, salt, privateKey, verifier); } /// /// Derive a private key for a user. /// /// Salt. - /// Email. + /// Username. /// Hashed password string. /// Private key as string. - public static string DerivePrivateKey(string salt, string email, string passwordHashString) + public static string DerivePrivateKey(string salt, string username, string passwordHashString) { var client = new SrpClient(); - return client.DerivePrivateKey(salt, email, passwordHashString); + return client.DerivePrivateKey(salt, username, passwordHashString); } /// @@ -76,16 +76,16 @@ public static class Srp /// Client ephemeral secret. /// Server public ephemeral. /// Salt. - /// Email. + /// Username. /// session. - public static SrpSession DeriveSessionClient(string privateKey, string clientSecretEphemeral, string serverEphemeralPublic, string salt, string email) + public static SrpSession DeriveSessionClient(string privateKey, string clientSecretEphemeral, string serverEphemeralPublic, string salt, string username) { var client = new SrpClient(); return client.DeriveSession( clientSecretEphemeral, serverEphemeralPublic, salt, - email, + username, privateKey); } @@ -95,18 +95,18 @@ public static class Srp /// serverEphemeralSecret. /// clientEphemeralPublic. /// Salt. - /// Email. + /// Username. /// Verifier. /// Client session proof. /// SrpSession. - public static SrpSession DeriveSessionServer(string serverEphemeralSecret, string clientEphemeralPublic, string salt, string email, string verifier, string clientSessionProof) + public static SrpSession DeriveSessionServer(string serverEphemeralSecret, string clientEphemeralPublic, string salt, string username, string verifier, string clientSessionProof) { var server = new SrpServer(); return server.DeriveSession( serverEphemeralSecret, clientEphemeralPublic, salt, - email, + username, verifier, clientSessionProof); }