| | | 1 | | using Anichron.API.Settings; |
| | | 2 | | using Microsoft.Extensions.Options; |
| | | 3 | | using System.Net.Mail; |
| | | 4 | | |
| | | 5 | | namespace Anichron.API.Services; |
| | | 6 | | |
| | | 7 | | public interface IRegistrationValidator |
| | | 8 | | { |
| | | 9 | | AuthError? ValidateIdentity(string username, string email); |
| | | 10 | | Task<AuthError?> ValidateAsync(string username, string email, string password, CancellationToken ct); |
| | | 11 | | Task<AuthError?> ValidatePasswordAsync(string password, CancellationToken ct); |
| | | 12 | | } |
| | | 13 | | |
| | 42 | 14 | | public sealed class RegistrationValidator( |
| | 42 | 15 | | IOptions<PasswordPolicy> passwordPolicyOptions, |
| | 42 | 16 | | IOptions<UsernamePolicy> usernamePolicyOptions, |
| | 42 | 17 | | IPwnedPasswordClient pwnedClient) : IRegistrationValidator |
| | | 18 | | { |
| | 42 | 19 | | private readonly PasswordPolicy passwordPolicy = passwordPolicyOptions.Value; |
| | 42 | 20 | | private readonly UsernamePolicy usernamePolicy = usernamePolicyOptions.Value; |
| | | 21 | | |
| | | 22 | | public AuthError? ValidateIdentity(string username, string email) |
| | 34 | 23 | | { |
| | 34 | 24 | | if (username.Length < usernamePolicy.MinLength |
| | 34 | 25 | | || username.Length > usernamePolicy.MaxLength |
| | 34 | 26 | | || !UsernamePolicy.AllowedCharacters().IsMatch(username)) |
| | 11 | 27 | | { |
| | 11 | 28 | | return AuthError.InvalidUsername; |
| | | 29 | | } |
| | | 30 | | |
| | 23 | 31 | | if (!IsValidEmail(email)) |
| | 10 | 32 | | return AuthError.InvalidEmail; |
| | | 33 | | |
| | 13 | 34 | | return null; |
| | 34 | 35 | | } |
| | | 36 | | |
| | | 37 | | public Task<AuthError?> ValidateAsync(string username, string email, string password, CancellationToken ct) |
| | 22 | 38 | | => ValidateIdentity(username, email) is { } identityError |
| | 22 | 39 | | ? Task.FromResult<AuthError?>(identityError) |
| | 22 | 40 | | : ValidatePasswordRulesAsync(password, ct); |
| | | 41 | | |
| | | 42 | | public Task<AuthError?> ValidatePasswordAsync(string password, CancellationToken ct) |
| | 8 | 43 | | => ValidatePasswordRulesAsync(password, ct); |
| | | 44 | | |
| | | 45 | | private async Task<AuthError?> ValidatePasswordRulesAsync(string password, CancellationToken ct) |
| | 18 | 46 | | { |
| | 18 | 47 | | if (password.Length < passwordPolicy.MinLength) |
| | 4 | 48 | | return AuthError.PasswordTooShort; |
| | | 49 | | |
| | 14 | 50 | | if (password.Length > passwordPolicy.MaxLength) |
| | 2 | 51 | | return AuthError.PasswordTooLong; |
| | | 52 | | |
| | | 53 | | // IDE0046 suppressed: collapsing an async condition into a ternary reduces readability |
| | | 54 | | #pragma warning disable IDE0046 |
| | 12 | 55 | | if (passwordPolicy.CheckPwnedPasswords && await pwnedClient.IsPwnedAsync(password, ct)) |
| | | 56 | | #pragma warning restore IDE0046 |
| | 2 | 57 | | return AuthError.PasswordPwned; |
| | | 58 | | |
| | 8 | 59 | | return null; |
| | 16 | 60 | | } |
| | | 61 | | |
| | | 62 | | private static bool IsValidEmail(string email) |
| | 23 | 63 | | { |
| | 23 | 64 | | if (email.Length > AppDefaults.Email.MaxLength) |
| | 1 | 65 | | return false; |
| | | 66 | | try |
| | 22 | 67 | | { |
| | 22 | 68 | | var mailAddress = new MailAddress(email.Trim()); |
| | 14 | 69 | | return mailAddress.Address.Equals(email.Trim(), StringComparison.OrdinalIgnoreCase); |
| | | 70 | | } |
| | 8 | 71 | | catch (Exception ex) when (ex is FormatException or ArgumentException) |
| | 8 | 72 | | { |
| | 8 | 73 | | return false; |
| | | 74 | | } |
| | 23 | 75 | | } |
| | | 76 | | } |