1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-12 07:02:16 -05:00
Ike 3f95513d11
[PM-19029][PM-19203] Addressing UserService tech debt around ITwoFactorIsEnabledQuery (#5754)
* fix : split out the interface from the TwoFactorAuthenticationValidator into separate file.
* fix: replacing IUserService.TwoFactorEnabled with ITwoFactorEnabledQuery
* fix: combined logic for both bulk and single user look ups for TwoFactorIsEnabledQuery.
* fix: return two factor provider enabled on CanGenerate() method.

* tech debt: modfifying MFA providers to call the database less to validate if two factor is enabled. 
* tech debt: removed unused service from AuthenticatorTokenProvider

* doc: added documentation to ITwoFactorProviderUsers
* doc: updated comments for TwoFactorIsEnabled impl

* test: fixing tests for ITwoFactorIsEnabledQuery
* test: updating tests to have correct DI and removing test for automatic email of TOTP.
* test: adding better test coverage
2025-05-09 11:39:57 -04:00

243 lines
7.3 KiB
C#

using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Enums;
using Bit.Core.Tools.Entities;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Identity;
#nullable enable
namespace Bit.Core.Entities;
public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser, IReferenceable
{
private Dictionary<TwoFactorProviderType, TwoFactorProvider>? _twoFactorProviders;
public Guid Id { get; set; }
[MaxLength(50)]
public string? Name { get; set; }
[Required]
[MaxLength(256)]
public string Email { get; set; } = null!;
public bool EmailVerified { get; set; }
[MaxLength(300)]
public string? MasterPassword { get; set; }
[MaxLength(50)]
public string? MasterPasswordHint { get; set; }
[MaxLength(10)]
public string Culture { get; set; } = "en-US";
[Required]
[MaxLength(50)]
public string SecurityStamp { get; set; } = null!;
public string? TwoFactorProviders { get; set; }
[MaxLength(32)]
public string? TwoFactorRecoveryCode { get; set; }
public string? EquivalentDomains { get; set; }
public string? ExcludedGlobalEquivalentDomains { get; set; }
public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow;
public string? Key { get; set; }
public string? PublicKey { get; set; }
public string? PrivateKey { get; set; }
public bool Premium { get; set; }
public DateTime? PremiumExpirationDate { get; set; }
public DateTime? RenewalReminderDate { get; set; }
public long? Storage { get; set; }
public short? MaxStorageGb { get; set; }
public GatewayType? Gateway { get; set; }
[MaxLength(50)]
public string? GatewayCustomerId { get; set; }
[MaxLength(50)]
public string? GatewaySubscriptionId { get; set; }
public string? ReferenceData { get; set; }
[MaxLength(100)]
public string? LicenseKey { get; set; }
[Required]
[MaxLength(30)]
public string ApiKey { get; set; } = null!;
public KdfType Kdf { get; set; } = KdfType.PBKDF2_SHA256;
public int KdfIterations { get; set; } = AuthConstants.PBKDF2_ITERATIONS.Default;
public int? KdfMemory { get; set; }
public int? KdfParallelism { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
public bool ForcePasswordReset { get; set; }
public bool UsesKeyConnector { get; set; }
public int FailedLoginCount { get; set; }
public DateTime? LastFailedLoginDate { get; set; }
[MaxLength(7)]
public string? AvatarColor { get; set; }
public DateTime? LastPasswordChangeDate { get; set; }
public DateTime? LastKdfChangeDate { get; set; }
public DateTime? LastKeyRotationDate { get; set; }
public DateTime? LastEmailChangeDate { get; set; }
public bool VerifyDevices { get; set; } = true;
public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
public string? BillingEmailAddress()
{
return Email?.ToLowerInvariant()?.Trim();
}
public string? BillingName()
{
return Name;
}
public string SubscriberName()
{
return string.IsNullOrWhiteSpace(Name) ? Email : Name;
}
public string BraintreeCustomerIdPrefix()
{
return "u";
}
public string BraintreeIdField()
{
return "user_id";
}
public string BraintreeCloudRegionField()
{
return "region";
}
public string GatewayIdField()
{
return "userId";
}
public bool IsOrganization() => false;
public bool IsUser()
{
return true;
}
public string SubscriberType()
{
return "Subscriber";
}
public bool IsExpired() => PremiumExpirationDate.HasValue && PremiumExpirationDate.Value <= DateTime.UtcNow;
/// <summary>
/// Deserializes the User.TwoFactorProviders property from JSON to the appropriate C# dictionary.
/// </summary>
/// <returns>Dictionary of TwoFactor providers</returns>
public Dictionary<TwoFactorProviderType, TwoFactorProvider>? GetTwoFactorProviders()
{
if (string.IsNullOrWhiteSpace(TwoFactorProviders))
{
return null;
}
try
{
_twoFactorProviders ??=
JsonHelpers.LegacyDeserialize<Dictionary<TwoFactorProviderType, TwoFactorProvider>>(
TwoFactorProviders);
/*
U2F is no longer supported, and all users keys should have been migrated to WebAuthn.
To prevent issues with accounts being prompted for unsupported U2F we remove them.
This will probably exist in perpetuity since there is no way to know for sure if any
given user does or doesn't have this enabled. It is a non-zero chance.
*/
_twoFactorProviders?.Remove(TwoFactorProviderType.U2f);
return _twoFactorProviders;
}
catch (JsonException)
{
return null;
}
}
public Guid? GetUserId()
{
return Id;
}
public bool GetPremium()
{
return Premium;
}
/// <summary>
/// Serializes the C# object to the User.TwoFactorProviders property in JSON format.
/// </summary>
/// <param name="providers">Dictionary of Two Factor providers</param>
public void SetTwoFactorProviders(Dictionary<TwoFactorProviderType, TwoFactorProvider> providers)
{
// When replacing with system.text remember to remove the extra serialization in WebAuthnTokenProvider.
TwoFactorProviders = JsonHelpers.LegacySerialize(providers, JsonHelpers.LegacyEnumKeyResolver);
_twoFactorProviders = providers;
}
/// <summary>
/// Checks if the user has a specific TwoFactorProvider configured. If a user has a premium TwoFactor
/// configured it will still be found, even if the user's premium subscription has ended.
/// </summary>
/// <param name="provider">TwoFactor provider being searched for</param>
/// <returns>TwoFactorProvider if found; null otherwise.</returns>
public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider)
{
var providers = GetTwoFactorProviders();
if (providers == null || !providers.TryGetValue(provider, out var value))
{
return null;
}
return value;
}
public long StorageBytesRemaining()
{
if (!MaxStorageGb.HasValue)
{
return 0;
}
return StorageBytesRemaining(MaxStorageGb.Value);
}
public long StorageBytesRemaining(short maxStorageGb)
{
var maxStorageBytes = maxStorageGb * 1073741824L;
if (!Storage.HasValue)
{
return maxStorageBytes;
}
return maxStorageBytes - Storage.Value;
}
public IdentityUser ToIdentityUser(bool twoFactorEnabled)
{
return new IdentityUser
{
Id = Id.ToString(),
Email = Email,
NormalizedEmail = Email,
EmailConfirmed = EmailVerified,
UserName = Email,
NormalizedUserName = Email,
TwoFactorEnabled = twoFactorEnabled,
SecurityStamp = SecurityStamp
};
}
public bool HasMasterPassword()
{
return MasterPassword != null;
}
}