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