using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; /// /// Base class for setting up members or users to use 2FA. /// internal abstract class TwoFactorLoginServiceBase { private readonly ITwoFactorLoginService _twoFactorLoginService; private readonly ICoreScopeProvider _scopeProvider; private readonly IDictionary _twoFactorSetupGenerators; protected TwoFactorLoginServiceBase(ITwoFactorLoginService twoFactorLoginService, IEnumerable twoFactorSetupGenerators, ICoreScopeProvider scopeProvider) { _twoFactorLoginService = twoFactorLoginService; _scopeProvider = scopeProvider; _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x => x.ProviderName); } public virtual async Task> DisableAsync(Guid userKey, string providerName) { var result = await _twoFactorLoginService.DisableAsync(userKey, providerName); return result ? Attempt.Succeed(TwoFactorOperationStatus.Success) : Attempt.Fail(TwoFactorOperationStatus.ProviderNameNotFound); } /// /// Gets the two factor providers on a specific user. /// public virtual async Task, TwoFactorOperationStatus>> GetProviderNamesAsync(Guid userKey) { IEnumerable allProviders = _twoFactorLoginService.GetAllProviderNames(); var userProviders = (await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(userKey)).ToHashSet(); IEnumerable result = allProviders.Select(x => new UserTwoFactorProviderModel(x, userProviders.Contains(x))); return Attempt.SucceedWithStatus(TwoFactorOperationStatus.Success, result); } /// /// Generates a new random unique secret. /// /// The random secret protected virtual string GenerateSecret() => Guid.NewGuid().ToString(); public virtual async Task> GetSetupInfoAsync(Guid userOrMemberKey, string providerName) { var secret = await _twoFactorLoginService.GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); // Dont allow to generate a new secrets if user already has one if (!string.IsNullOrEmpty(secret)) { return Attempt.FailWithStatus(TwoFactorOperationStatus.ProviderAlreadySetup, new NoopSetupTwoFactorModel()); } secret = GenerateSecret(); if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) { return Attempt.FailWithStatus(TwoFactorOperationStatus.ProviderNameNotFound, new NoopSetupTwoFactorModel()); } ISetupTwoFactorModel result= await generator.GetSetupDataAsync(userOrMemberKey, secret); return Attempt.SucceedWithStatus(TwoFactorOperationStatus.Success, result); } public virtual async Task> ValidateAndSaveAsync( string providerName, Guid userOrMemberKey, string secret, string code) { using var scope = _scopeProvider.CreateCoreScope(); if ((await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(userOrMemberKey)).Contains(providerName)) { return Attempt.Fail(TwoFactorOperationStatus.ProviderAlreadySetup); } bool valid; try { valid = _twoFactorLoginService.ValidateTwoFactorSetup(providerName, secret, code); } catch (InvalidOperationException) { return Attempt.Fail(TwoFactorOperationStatus.ProviderNameNotFound); } if (valid is false) { return Attempt.Fail(TwoFactorOperationStatus.InvalidCode); } var twoFactorLogin = new TwoFactorLogin { Confirmed = true, Secret = secret, UserOrMemberKey = userOrMemberKey, ProviderName = providerName, }; await _twoFactorLoginService.SaveAsync(twoFactorLogin); scope.Complete(); return Attempt.Succeed(TwoFactorOperationStatus.Success); } /// /// Disables 2FA with Code. /// public async Task> DisableByCodeAsync(string providerName, Guid userOrMemberKey, string code) { var secret = await _twoFactorLoginService.GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) { return Attempt.Fail(TwoFactorOperationStatus.ProviderNameNotFound); } var isValid = secret is not null && generator.ValidateTwoFactorPIN(secret, code); if (!isValid) { return Attempt.Fail(TwoFactorOperationStatus.InvalidCode); } var success = await _twoFactorLoginService.DisableAsync(userOrMemberKey, providerName); return success ? Attempt.Succeed(TwoFactorOperationStatus.Success) : Attempt.Fail(TwoFactorOperationStatus.InvalidCode); } }