From 685a05827e797c22d2daff00fb5a6b8a944b1b8b Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 28 Mar 2025 11:16:03 +0100 Subject: [PATCH] Adds MemberTwoFactorLoginService. (#18810) --- .../DependencyInjection/UmbracoBuilder.cs | 1 + .../Services/IMemberTwoFactorLoginService.cs | 37 ++++++++++ .../Services/ITwoFactorLoginService.cs | 6 +- .../Services/MemberTwoFactorLoginService.cs | 72 +++++++++++++++++++ .../Services/UserTwoFactorLoginService.cs | 6 +- 5 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Core/Services/IMemberTwoFactorLoginService.cs create mode 100644 src/Umbraco.Core/Services/MemberTwoFactorLoginService.cs diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 3d36c67d3a..24cc907339 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -422,6 +422,7 @@ namespace Umbraco.Cms.Core.DependencyInjection // Two factor providers Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); // Add Query services Services.AddUnique(); diff --git a/src/Umbraco.Core/Services/IMemberTwoFactorLoginService.cs b/src/Umbraco.Core/Services/IMemberTwoFactorLoginService.cs new file mode 100644 index 0000000000..3a7ec99c8e --- /dev/null +++ b/src/Umbraco.Core/Services/IMemberTwoFactorLoginService.cs @@ -0,0 +1,37 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +/// +/// A member specific Two factor service, that ensures the member exists before doing the job. +/// +public interface IMemberTwoFactorLoginService +{ + /// + /// Disables a specific two factor provider on a specific member. + /// + Task> DisableAsync(Guid memberKey, string providerName); + + /// + /// Gets the two factor providers on a specific member. + /// + Task, TwoFactorOperationStatus>> GetProviderNamesAsync(Guid memberKey); + + /// + /// The returned type can be anything depending on the setup providers. You will need to cast it to the type handled by + /// the provider. + /// + Task> GetSetupInfoAsync(Guid memberKey, string providerName); + + /// + /// Validates and Saves. + /// + Task> ValidateAndSaveAsync(string providerName, Guid memberKey, string modelSecret, string modelCode); + + /// + /// Disables 2FA with Code. + /// + Task> DisableByCodeAsync(string providerName, Guid memberKey, string code); +} diff --git a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs index a5ad0d84e8..01558376e3 100644 --- a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs +++ b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs @@ -29,7 +29,7 @@ public interface ITwoFactorLoginService : IService /// The returned type can be anything depending on the setup providers. You will need to cast it to the type handled by /// the provider. /// - [Obsolete("Use IUserTwoFactorLoginService.GetSetupInfoAsync. This will be removed in Umbraco 15.")] + [Obsolete("Use IUserTwoFactorLoginService.GetSetupInfoAsync or IMemberTwoFactorLoginService.GetSetupInfoAsync. Scheduled for removal in Umbraco 16.")] Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName); /// @@ -60,13 +60,13 @@ public interface ITwoFactorLoginService : IService /// /// Disables 2FA with Code. /// - [Obsolete("Use IUserTwoFactorLoginService.DisableByCodeAsync. This will be removed in Umbraco 15.")] + [Obsolete("Use IUserTwoFactorLoginService.DisableByCodeAsync or IMemberTwoFactorLoginService.DisableByCodeAsync. Scheduled for removal in Umbraco 16.")] Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code); /// /// Validates and Saves. /// - [Obsolete("Use IUserTwoFactorLoginService.ValidateAndSaveAsync. This will be removed in Umbraco 15.")] + [Obsolete("Use IUserTwoFactorLoginService.ValidateAndSaveAsync or IMemberTwoFactorLoginService.ValidateAndSaveAsync. Scheduled for removal in Umbraco 16.")] Task ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code); } diff --git a/src/Umbraco.Core/Services/MemberTwoFactorLoginService.cs b/src/Umbraco.Core/Services/MemberTwoFactorLoginService.cs new file mode 100644 index 0000000000..b21558b834 --- /dev/null +++ b/src/Umbraco.Core/Services/MemberTwoFactorLoginService.cs @@ -0,0 +1,72 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +/// +internal class MemberTwoFactorLoginService : TwoFactorLoginServiceBase, IMemberTwoFactorLoginService +{ + private readonly IMemberService _memberService; + + public MemberTwoFactorLoginService( + ITwoFactorLoginService twoFactorLoginService, + IEnumerable twoFactorSetupGenerators, + IMemberService memberService, + ICoreScopeProvider scopeProvider) + : base(twoFactorLoginService, twoFactorSetupGenerators, scopeProvider) => + _memberService = memberService; + + /// + public override async Task> DisableAsync(Guid memberKey, string providerName) + { + IMember? member = _memberService.GetByKey(memberKey); + + if (member is null) + { + return Attempt.Fail(TwoFactorOperationStatus.UserNotFound); + } + + return await base.DisableAsync(memberKey, providerName); + } + + /// + public override async Task, TwoFactorOperationStatus>> GetProviderNamesAsync(Guid memberKey) + { + IMember? member = _memberService.GetByKey(memberKey); + + if (member is null) + { + return Attempt.FailWithStatus(TwoFactorOperationStatus.UserNotFound, Enumerable.Empty()); + } + + return await base.GetProviderNamesAsync(memberKey); + } + + /// + public override async Task> GetSetupInfoAsync(Guid memberKey, string providerName) + { + IMember? member = _memberService.GetByKey(memberKey); + + if (member is null) + { + return Attempt.FailWithStatus(TwoFactorOperationStatus.UserNotFound, new NoopSetupTwoFactorModel()); + } + + return await base.GetSetupInfoAsync(memberKey, providerName); + } + + /// + public override async Task> ValidateAndSaveAsync(string providerName, Guid memberKey, string secret, string code) + { + IMember? member = _memberService.GetByKey(memberKey); + + if (member is null) + { + return Attempt.Fail(TwoFactorOperationStatus.UserNotFound); + } + + return await base.ValidateAndSaveAsync(providerName, memberKey, secret, code); + } +} diff --git a/src/Umbraco.Core/Services/UserTwoFactorLoginService.cs b/src/Umbraco.Core/Services/UserTwoFactorLoginService.cs index 89241d31f2..10a9de140f 100644 --- a/src/Umbraco.Core/Services/UserTwoFactorLoginService.cs +++ b/src/Umbraco.Core/Services/UserTwoFactorLoginService.cs @@ -32,7 +32,7 @@ internal class UserTwoFactorLoginService : TwoFactorLoginServiceBase, IUserTwoFa return await base.DisableAsync(userKey, providerName); } - /// + /// public override async Task, TwoFactorOperationStatus>> GetProviderNamesAsync(Guid userKey) { IUser? user = await _userService.GetAsync(userKey); @@ -45,7 +45,7 @@ internal class UserTwoFactorLoginService : TwoFactorLoginServiceBase, IUserTwoFa return await base.GetProviderNamesAsync(userKey); } - /// + /// public override async Task> GetSetupInfoAsync(Guid userKey, string providerName) { IUser? user = await _userService.GetAsync(userKey); @@ -58,7 +58,7 @@ internal class UserTwoFactorLoginService : TwoFactorLoginServiceBase, IUserTwoFa return await base.GetSetupInfoAsync(userKey, providerName); } - /// + /// public override async Task> ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code) { IUser? user = await _userService.GetAsync(userKey);