From a95a876e3f0cf8ac459926dea48ce7201f40ca10 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 28 May 2024 09:36:40 -0700 Subject: [PATCH] Allow any user to manage two-factor (#16130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Zoltán Lehóczky --- .../OrchardCore.Admin/AdminFilter.cs | 6 +- .../OrchardCore.Admin/Permissions.cs | 6 +- .../Controllers/UserPickerAdminController.cs | 2 +- .../OrchardCore.ContentFields/Manifest.cs | 7 +- .../OrchardCore.ContentFields/Startup.cs | 3 +- .../OrchardCore.Demo/Manifest.cs | 7 +- .../Controllers/AdminController.cs | 7 +- .../OrchardCore.Notifications/Startup.cs | 3 +- .../Views/UserNotificationNavbar.cshtml | 12 +- .../OrchardCore.ReCaptcha/Manifest.cs | 7 +- .../OrchardCore.Users/AdminMenu.cs | 2 +- .../Controllers/AccountController.cs | 22 +-- .../Controllers/AuthenticatorAppController.cs | 15 +- .../EmailAuthenticatorController.cs | 183 ------------------ .../EmailConfirmationController.cs | 107 ++++++++++ .../Controllers/RegistrationController.cs | 56 +----- .../Controllers/SmsAuthenticatorController.cs | 49 +---- .../TwoFactorAuthenticationBaseController.cs | 9 +- .../TwoFactorAuthenticationController.cs | 34 ++-- .../TwoFactorLoginSettingsDisplayDriver.cs | 11 +- .../ControllerExtensions.cs | 18 +- ...FactorAuthenticationAuthorizationFilter.cs | 60 ++++-- .../OrchardCore.Users/Manifest.cs | 15 +- .../RoleTwoFactorAuthenticationHandler.cs | 18 +- .../TwoFactorAuthenticationClaimsProvider.cs | 5 +- .../Services/UsersThemeSelector.cs | 26 ++- .../OrchardCore.Users/Startup.cs | 126 +++++++++--- .../TwoFactorAuthenticationStartup.cs | 71 +++++++ .../EnableEmailAuthenticatorViewModel.cs | 9 - .../Views/AuthenticatorApp/Index.cshtml | 9 +- .../Views/AuthenticatorApp/Reset.cshtml | 4 + .../Views/EmailAuthenticator/Index.cshtml | 17 -- .../EmailAuthenticator/RequestCode.cshtml | 20 -- .../ConfirmEmail.cshtml | 0 .../ConfirmEmailSent.cshtml | 0 .../TwoFactorMethod.Email.Actions.cshtml | 16 +- .../ResetPasswordConfirmation.cshtml | 2 +- .../Views/SmsAuthenticator/Index.cshtml | 5 + .../SmsAuthenticator/ValidateCode.cshtml | 5 + .../Views/TemplateUserConfirmEmail.cshtml | 3 +- .../Views/TemplateUserLostPassword.cshtml | 3 +- .../DisableTwoFactorAuthentication.cshtml | 4 + .../GenerateRecoveryCodes.cshtml | 4 + .../TwoFactorAuthentication/Index.cshtml | 2 + .../LoginWithRecoveryCode.cshtml | 2 +- .../LoginWithTwoFactorAuthentication.cshtml | 7 +- .../ShowRecoveryCodes.cshtml | 4 + .../Views/TwoFactorLoginSettings.Edit.cshtml | 10 + .../OrchardCore.Users/Views/UserMenu.cshtml | 2 +- .../Views/UserMenuItems-ChangeEmail.cshtml | 2 +- .../Views/UserMenuItems-ExternalLogins.cshtml | 2 +- .../Views/UserMenuItems-Profile.cshtml | 4 +- .../Views/UserMenuItems-SignOut.cshtml | 4 +- .../Views/UserMenuItems-TwoFactor.cshtml | 2 +- .../Workflows/Activities/RegisterUserTask.cs | 2 +- .../TheAdmin/Views/Layout-TwoFactor.cshtml | 53 +++++ .../TheAdmin/Views/Layout.cshtml | 2 +- .../Views/UserNotificationNavbar.cshtml | 12 +- .../AdminPermissions.cs | 9 + .../Events/ITwoFactorAuthenticationHandler.cs | 5 +- ...oFactorAuthenticationHandlerCoordinator.cs | 3 +- .../Models/TwoFactorLoginSettings.cs | 2 + .../DefaultTwoFactorAuthenticationHandler.cs | 2 +- ...oFactorAuthenticationHandlerCoordinator.cs | 4 +- .../OrchardCore.Users.Core/UserConstants.cs | 2 - src/docs/releases/2.0.0.md | 2 + .../RegistrationControllerTests.cs | 2 +- 67 files changed, 620 insertions(+), 509 deletions(-) delete mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailConfirmationController.cs rename src/OrchardCore.Modules/OrchardCore.Users/{Controllers => Extensions}/ControllerExtensions.cs (90%) delete mode 100644 src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EnableEmailAuthenticatorViewModel.cs delete mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/EmailAuthenticator/Index.cshtml delete mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/EmailAuthenticator/RequestCode.cshtml rename src/OrchardCore.Modules/OrchardCore.Users/Views/{Registration => EmailConfirmation}/ConfirmEmail.cshtml (100%) rename src/OrchardCore.Modules/OrchardCore.Users/Views/{Registration => EmailConfirmation}/ConfirmEmailSent.cshtml (100%) create mode 100644 src/OrchardCore.Themes/TheAdmin/Views/Layout-TwoFactor.cshtml create mode 100644 src/OrchardCore/OrchardCore.Admin.Abstractions/AdminPermissions.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Admin/AdminFilter.cs b/src/OrchardCore.Modules/OrchardCore.Admin/AdminFilter.cs index 0dca76e6a94..4439e7ec699 100644 --- a/src/OrchardCore.Modules/OrchardCore.Admin/AdminFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Admin/AdminFilter.cs @@ -25,7 +25,9 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context if (!await AuthorizeAsync(context.HttpContext)) { - context.Result = context.HttpContext.User?.Identity?.IsAuthenticated ?? false ? (IActionResult)new ForbidResult() : new ChallengeResult(); + context.Result = context.HttpContext.User?.Identity?.IsAuthenticated ?? false + ? new ForbidResult() + : new ChallengeResult(); return; } @@ -55,7 +57,7 @@ private Task AuthorizeAsync(Microsoft.AspNetCore.Http.HttpContext context) { if (AdminAttribute.IsApplied(context)) { - return _authorizationService.AuthorizeAsync(context.User, Permissions.AccessAdminPanel); + return _authorizationService.AuthorizeAsync(context.User, AdminPermissions.AccessAdminPanel); } return Task.FromResult(true); diff --git a/src/OrchardCore.Modules/OrchardCore.Admin/Permissions.cs b/src/OrchardCore.Modules/OrchardCore.Admin/Permissions.cs index 334dfb7222d..e4bfb32c486 100644 --- a/src/OrchardCore.Modules/OrchardCore.Admin/Permissions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Admin/Permissions.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; using OrchardCore.Security.Permissions; @@ -6,11 +7,12 @@ namespace OrchardCore.Admin; public class Permissions : IPermissionProvider { - public static readonly Permission AccessAdminPanel = new("AccessAdminPanel", "Access admin panel"); + [Obsolete("This property will be removed in future release. Instead use 'AdminPermissions.AccessAdminPanel'.")] + public static readonly Permission AccessAdminPanel = AdminPermissions.AccessAdminPanel; private readonly IEnumerable _allPermissions = [ - AccessAdminPanel, + AdminPermissions.AccessAdminPanel, ]; public Task> GetPermissionsAsync() diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Controllers/UserPickerAdminController.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/Controllers/UserPickerAdminController.cs index 855fb3ec6a4..4107e4e3246 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Controllers/UserPickerAdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Controllers/UserPickerAdminController.cs @@ -15,7 +15,7 @@ namespace OrchardCore.ContentFields.Controllers { - [RequireFeatures("OrchardCore.Users")] + [RequireFeatures(OrchardCore.Users.UserConstants.Features.Users)] [Admin] public class UserPickerAdminController : Controller { diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/Manifest.cs index 4a2c6beb0d4..aca865bfaad 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Manifest.cs @@ -1,4 +1,5 @@ using OrchardCore.Modules.Manifest; +using OrchardCore.Users; [assembly: Module( Name = "Content Fields", @@ -29,5 +30,9 @@ Name = "Content Fields Indexing (SQL) - User Picker", Category = "Content Management", Description = "User Picker Content Fields Indexing module adds database indexing for user picker fields.", - Dependencies = ["OrchardCore.ContentFields", "OrchardCore.Users"] + Dependencies = + [ + "OrchardCore.ContentFields", + UserConstants.Features.Users, + ] )] diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Startup.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/Startup.cs index d49cde5fd74..d575c8cf24a 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Startup.cs @@ -16,6 +16,7 @@ using OrchardCore.Data.Migration; using OrchardCore.Indexing; using OrchardCore.Modules; +using OrchardCore.Users; namespace OrchardCore.ContentFields { @@ -176,7 +177,7 @@ public override void ConfigureServices(IServiceCollection services) } } - [RequireFeatures("OrchardCore.Users")] + [RequireFeatures(UserConstants.Features.Users)] public class UserPickerStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) diff --git a/src/OrchardCore.Modules/OrchardCore.Demo/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Demo/Manifest.cs index 9aa156334b6..e0144244262 100644 --- a/src/OrchardCore.Modules/OrchardCore.Demo/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Demo/Manifest.cs @@ -1,4 +1,5 @@ using OrchardCore.Modules.Manifest; +using OrchardCore.Users; [assembly: Module( Name = "Orchard Demo", @@ -12,7 +13,11 @@ Id = "OrchardCore.Demo", Description = "Test", Category = "Samples", - Dependencies = ["OrchardCore.Users", "OrchardCore.Contents"] + Dependencies = + [ + UserConstants.Features.Users, + "OrchardCore.Contents", + ] )] [assembly: Feature( diff --git a/src/OrchardCore.Modules/OrchardCore.Notifications/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Notifications/Controllers/AdminController.cs index 8bcb805bd34..c6f448464a4 100644 --- a/src/OrchardCore.Modules/OrchardCore.Notifications/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Notifications/Controllers/AdminController.cs @@ -46,7 +46,6 @@ public class AdminController : Controller, IUpdateModel public AdminController( IAuthorizationService authorizationService, ISession session, - IOptions pagerOptions, IDisplayManager notificationDisplayManager, INotificationsAdminListQueryService notificationsAdminListQueryService, @@ -143,7 +142,8 @@ public async Task List( return View(shapeViewModel); } - [HttpPost, ActionName(nameof(List))] + [HttpPost] + [ActionName(nameof(List))] [FormValueRequired("submit.Filter")] public async Task ListFilterPOST(ListNotificationOptions options) { @@ -162,7 +162,8 @@ public async Task ListFilterPOST(ListNotificationOptions options) return RedirectToAction(nameof(List), options.RouteValues); } - [HttpPost, ActionName(nameof(List))] + [HttpPost] + [ActionName(nameof(List))] [FormValueRequired("submit.BulkAction")] public async Task ListPOST(ListNotificationOptions options, IEnumerable itemIds) { diff --git a/src/OrchardCore.Modules/OrchardCore.Notifications/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Notifications/Startup.cs index de3a02dea9e..81131a4f719 100644 --- a/src/OrchardCore.Modules/OrchardCore.Notifications/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Notifications/Startup.cs @@ -20,6 +20,7 @@ using OrchardCore.Notifications.Services; using OrchardCore.ResourceManagement; using OrchardCore.Security.Permissions; +using OrchardCore.Users; using OrchardCore.Users.Models; using OrchardCore.Workflows.Helpers; using YesSql.Filters.Query; @@ -86,7 +87,7 @@ public override void ConfigureServices(IServiceCollection services) } } -[RequireFeatures("OrchardCore.Workflows", "OrchardCore.Users", "OrchardCore.Contents")] +[RequireFeatures("OrchardCore.Workflows", UserConstants.Features.Users, "OrchardCore.Contents")] public class UsersWorkflowStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) diff --git a/src/OrchardCore.Modules/OrchardCore.Notifications/Views/UserNotificationNavbar.cshtml b/src/OrchardCore.Modules/OrchardCore.Notifications/Views/UserNotificationNavbar.cshtml index bdcc1666d18..e9ae26c3af3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Notifications/Views/UserNotificationNavbar.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Notifications/Views/UserNotificationNavbar.cshtml @@ -1,9 +1,12 @@ @using Microsoft.AspNetCore.Mvc.Localization @using OrchardCore.DisplayManagement.ModelBinding +@using Microsoft.AspNetCore.Authorization +@using OrchardCore.Admin @using OrchardCore.Notifications.Endpoints.Management @inject IDisplayManager NotificationDisplayDriver @inject IUpdateModelAccessor UpdateModelAccessor +@inject IAuthorizationService AuthorizationService @model UserNotificationNavbarViewModel @@ -53,9 +56,12 @@
  • -
  • - @T["Notification Center"] -
  • + @if (await AuthorizationService.AuthorizeAsync(ViewContext.HttpContext.User, AdminPermissions.AccessAdminPanel)) + { +
  • + @T["Notification Center"] +
  • + } diff --git a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Manifest.cs index 4d091b73954..9bd65b5e6a6 100644 --- a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Manifest.cs @@ -1,4 +1,5 @@ using OrchardCore.Modules.Manifest; +using OrchardCore.Users; [assembly: Module( Name = "ReCaptcha", @@ -17,4 +18,8 @@ Name = "ReCaptcha Users", Description = "Provides ReCaptcha functionality to harness login, register, forgot password and forms against robots.", Category = "Security", - Dependencies = ["OrchardCore.ReCaptcha", "OrchardCore.Users"])] + Dependencies = + [ + "OrchardCore.ReCaptcha", + UserConstants.Features.Users, + ])] diff --git a/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs index a129fcee209..9430558da4f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs @@ -36,7 +36,7 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) .Add(S["Users"], S["Users"].PrefixPosition(), users => users .AddClass("users") .Id("users") - .Action("Index", "Admin", "OrchardCore.Users") + .Action("Index", "Admin", UserConstants.Features.Users) .Permission(CommonPermissions.ListUsers) .Resource(new User()) .LocalNav() diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs index 221e2402247..2beb653a08f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs @@ -398,11 +398,11 @@ public async Task ExternalLoginCallback(string returnUrl = null, { if (iUser is User userToLink && registrationSettings.UsersMustValidateEmail && !userToLink.EmailConfirmed) { - return RedirectToAction(nameof(RegistrationController.ConfirmEmailSent), + return RedirectToAction(nameof(EmailConfirmationController.ConfirmEmailSent), new { - Area = "OrchardCore.Users", - Controller = typeof(RegistrationController).ControllerName(), + Area = UserConstants.Features.Users, + Controller = typeof(EmailConfirmationController).ControllerName(), ReturnUrl = returnUrl, }); } @@ -462,11 +462,11 @@ public async Task ExternalLoginCallback(string returnUrl = null, { if (registrationSettings.UsersMustValidateEmail && !user.EmailConfirmed) { - return RedirectToAction(nameof(RegistrationController.ConfirmEmailSent), + return RedirectToAction(nameof(EmailConfirmationController.ConfirmEmailSent), new { - Area = "OrchardCore.Users", - Controller = typeof(RegistrationController).ControllerName(), + Area = UserConstants.Features.Users, + Controller = typeof(EmailConfirmationController).ControllerName(), ReturnUrl = returnUrl, }); } @@ -476,7 +476,7 @@ public async Task ExternalLoginCallback(string returnUrl = null, return RedirectToAction(nameof(RegistrationController.RegistrationPending), new { - Area = "OrchardCore.Users", + Area = UserConstants.Features.Users, Controller = typeof(RegistrationController).ControllerName(), ReturnUrl = returnUrl, }); @@ -581,11 +581,11 @@ public async Task RegisterExternalLogin(RegisterExternalLoginView { if (settings.UsersMustValidateEmail && !user.EmailConfirmed) { - return RedirectToAction(nameof(RegistrationController.ConfirmEmailSent), + return RedirectToAction(nameof(EmailConfirmationController.ConfirmEmailSent), new { - Area = "OrchardCore.Users", - Controller = typeof(RegistrationController).ControllerName(), + Area = UserConstants.Features.Users, + Controller = typeof(EmailConfirmationController).ControllerName(), ReturnUrl = returnUrl, }); } @@ -595,7 +595,7 @@ public async Task RegisterExternalLogin(RegisterExternalLoginView return RedirectToAction(nameof(RegistrationController.RegistrationPending), new { - Area = "OrchardCore.Users", + Area = UserConstants.Features.Users, Controller = typeof(RegistrationController).ControllerName(), ReturnUrl = returnUrl, }); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs index fe383e00be7..0072a7f730b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs @@ -10,7 +10,6 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; -using OrchardCore.Admin; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Environment.Shell; using OrchardCore.Modules; @@ -21,10 +20,11 @@ namespace OrchardCore.Users.Controllers; -[Authorize, Admin, Feature(UserConstants.Features.AuthenticatorApp)] +[Authorize] +[Feature(UserConstants.Features.AuthenticatorApp)] public class AuthenticatorAppController : TwoFactorAuthenticationBaseController { - private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&digits={3}&issuer={0}"; + private const string _authenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&digits={3}&issuer={0}"; private readonly IdentityOptions _identityOptions; private readonly UrlEncoder _urlEncoder; @@ -59,7 +59,6 @@ public AuthenticatorAppController( _shellSettings = shellSettings; } - [Admin("Authenticator/Configure/App", "ConfigureAuthenticatorApp")] public async Task Index(string returnUrl) { var user = await UserManager.GetUserAsync(User); @@ -109,7 +108,6 @@ public async Task Index(EnableAuthenticatorViewModel model) return await RedirectToTwoFactorAsync(user); } - [Admin("Authenticator/Reset/App", "RemoveAuthenticatorApp")] public async Task Reset() { var user = await UserManager.GetUserAsync(User); @@ -122,14 +120,15 @@ public async Task Reset() var model = new ResetAuthenticatorViewModel() { - CanRemove = providers.Count > 1 || !await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync(), + CanRemove = providers.Count > 1 || !await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync(user), WillDisableTwoFactor = providers.Count == 1, }; return View(model); } - [HttpPost, ActionName(nameof(Reset))] + [HttpPost] + [ActionName(nameof(Reset))] public async Task ResetPost() { var user = await UserManager.GetUserAsync(User); @@ -195,7 +194,7 @@ private async Task GenerateQrCodeUriAsync(string displayName, string unf return string.Format( CultureInfo.InvariantCulture, #pragma warning disable CA1863 // Cache a 'CompositeFormat' for repeated use in this formatting operation - AuthenticatorUriFormat, + _authenticatorUriFormat, #pragma warning restore CA1863 _urlEncoder.Encode(issuer), _urlEncoder.Encode(displayName), diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs deleted file mode 100644 index 635b8bdf32f..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Fluid.Values; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Localization; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Localization; -using Microsoft.Extensions.Options; -using OrchardCore.Admin; -using OrchardCore.DisplayManagement.Notify; -using OrchardCore.Email; -using OrchardCore.Liquid; -using OrchardCore.Modules; -using OrchardCore.Settings; -using OrchardCore.Users.Events; -using OrchardCore.Users.Models; -using OrchardCore.Users.Services; -using OrchardCore.Users.ViewModels; - -namespace OrchardCore.Users.Controllers; - -[Authorize, Feature(UserConstants.Features.EmailAuthenticator)] -public class EmailAuthenticatorController : TwoFactorAuthenticationBaseController -{ - private readonly IUserService _userService; - private readonly IEmailService _emailService; - private readonly ILiquidTemplateManager _liquidTemplateManager; - private readonly HtmlEncoder _htmlEncoder; - - public EmailAuthenticatorController( - SignInManager signInManager, - UserManager userManager, - ISiteService siteService, - IHtmlLocalizer htmlLocalizer, - IStringLocalizer stringLocalizer, - IOptions twoFactorOptions, - INotifier notifier, - IDistributedCache distributedCache, - IUserService userService, - IEmailService emailService, - ILiquidTemplateManager liquidTemplateManager, - HtmlEncoder htmlEncoder, - ITwoFactorAuthenticationHandlerCoordinator twoFactorAuthenticationHandlerCoordinator) - : base( - userManager, - distributedCache, - signInManager, - twoFactorAuthenticationHandlerCoordinator, - notifier, - siteService, - htmlLocalizer, - stringLocalizer, - twoFactorOptions) - { - _userService = userService; - _emailService = emailService; - _liquidTemplateManager = liquidTemplateManager; - _htmlEncoder = htmlEncoder; - } - - [Admin("Authenticator/Configure/Email", "ConfigureEmailAuthenticator")] - public async Task Index() - { - var user = await UserManager.GetUserAsync(User); - if (user == null) - { - return UserNotFound(); - } - - if (await UserManager.IsEmailConfirmedAsync(user)) - { - return RedirectToTwoFactorIndex(); - } - - return View(); - } - - [HttpPost] - [Admin("Authenticator/Configure/Email/RequestCode", "ConfigureEmailAuthenticatorRequestCode")] - public async Task RequestCode() - { - var user = await UserManager.GetUserAsync(User); - if (user == null) - { - return UserNotFound(); - } - - if (await UserManager.IsEmailConfirmedAsync(user)) - { - return RedirectToTwoFactorIndex(); - } - - var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); - - var settings = (await SiteService.GetSiteSettingsAsync()).As(); - - var to = await UserManager.GetEmailAsync(user); - var subject = await GetSubjectAsync(settings, user, code); - var body = await GetBodyAsync(settings, user, code); - var result = await _emailService.SendAsync(to, subject, body); - - if (!result.Succeeded) - { - await Notifier.ErrorAsync(H["We are unable to send you an email at this time. Please try again later."]); - - return RedirectToAction(nameof(Index)); - } - - await Notifier.SuccessAsync(H["We have successfully sent an verification code to your email. Please retrieve the code from your email."]); - - var model = new EnableEmailAuthenticatorViewModel(); - - return View(model); - } - - [HttpPost] - [Admin("Authenticator/Configure/Email/ValidateCode", "ConfigureEmailAuthenticatorValidateCode")] - public async Task ValidateCode(EnableEmailAuthenticatorViewModel model) - { - var user = await UserManager.GetUserAsync(User); - if (user == null) - { - return UserNotFound(); - } - - if (!ModelState.IsValid) - { - return View(model); - } - - var result = await UserManager.ConfirmEmailAsync(user, StripToken(model.Code)); - - if (result.Succeeded) - { - await EnableTwoFactorAuthenticationAsync(user); - - await Notifier.SuccessAsync(H["Your email has been confirmed."]); - - return await RedirectToTwoFactorAsync(user); - } - - await Notifier.ErrorAsync(H["Unable to confirm your email. Please try again."]); - - return View(nameof(RequestCode), model); - } - - private Task GetSubjectAsync(EmailAuthenticatorLoginSettings settings, IUser user, string code) - { - var message = string.IsNullOrWhiteSpace(settings.Subject) - ? EmailAuthenticatorLoginSettings.DefaultSubject - : settings.Subject; - - return GetContentAsync(message, user, code); - } - - private Task GetBodyAsync(EmailAuthenticatorLoginSettings settings, IUser user, string code) - { - var message = string.IsNullOrWhiteSpace(settings.Body) - ? EmailAuthenticatorLoginSettings.DefaultBody - : settings.Body; - - return GetContentAsync(message, user, code); - } - - private async Task GetContentAsync(string message, IUser user, string code) - { - var result = await _liquidTemplateManager.RenderHtmlContentAsync(message, _htmlEncoder, null, - new Dictionary() - { - ["User"] = new ObjectValue(user), - ["Code"] = new StringValue(code), - }); - - using var writer = new StringWriter(); - result.WriteTo(writer, _htmlEncoder); - - return writer.ToString(); - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailConfirmationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailConfirmationController.cs new file mode 100644 index 00000000000..fce936c2857 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailConfirmationController.cs @@ -0,0 +1,107 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Localization; +using OrchardCore.DisplayManagement.Notify; +using OrchardCore.Mvc.Core.Utilities; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Controllers; + +public class EmailConfirmationController : Controller +{ + private readonly UserManager _userManager; + private readonly IAuthorizationService _authorizationService; + private readonly INotifier _notifier; + + protected readonly IHtmlLocalizer H; + protected readonly IStringLocalizer S; + + public EmailConfirmationController( + UserManager userManager, + IAuthorizationService authorizationService, + INotifier notifier, + IHtmlLocalizer htmlLocalizer, + IStringLocalizer stringLocalizer) + { + _userManager = userManager; + _authorizationService = authorizationService; + _notifier = notifier; + H = htmlLocalizer; + S = stringLocalizer; + } + + [AllowAnonymous] + public async Task ConfirmEmail(string userId, string code) + { + if (userId == null || code == null) + { + return NotFound(); + } + + var user = await _userManager.FindByIdAsync(userId); + + if (user == null) + { + return NotFound(); + } + + var result = await _userManager.ConfirmEmailAsync(user, code); + + if (result.Succeeded) + { + return View(); + } + + return NotFound(); + } + + [AllowAnonymous] + public IActionResult ConfirmEmailSent(string returnUrl = null) + => View(new { ReturnUrl = returnUrl }); + + [Authorize] + [HttpPost] + [ValidateAntiForgeryToken] + public async Task SendVerificationEmail(string id = null, string returnUrl = null) + { + var currentUserId = HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); + + if (string.IsNullOrEmpty(id)) + { + id = currentUserId; + } + + if (string.IsNullOrEmpty(id)) + { + return NotFound(); + } + + // Allow users to verify their own email without the 'ManageUsers' permission. + if (id != currentUserId && !await _authorizationService.AuthorizeAsync(User, CommonPermissions.ManageUsers)) + { + return Forbid(); + } + + var user = await _userManager.FindByIdAsync(id) as User; + + if (user == null) + { + return NotFound(); + } + + await this.SendEmailConfirmationTokenAsync(user, S["Confirm your account"]); + + await _notifier.SuccessAsync(H["Verification email sent."]); + + if (!string.IsNullOrWhiteSpace(returnUrl) && Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + + return RedirectToAction(nameof(AdminController.Index), typeof(AdminController).ControllerName()); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs index ba47f420407..120f748c02c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs @@ -10,6 +10,7 @@ using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Modules; +using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Settings; using OrchardCore.Users.Models; @@ -95,7 +96,7 @@ public async Task RegisterPOST(string returnUrl = null) { if (settings.UsersMustValidateEmail && !user.EmailConfirmed) { - return RedirectToAction(nameof(ConfirmEmailSent), new { ReturnUrl = returnUrl }); + return RedirectToAction(nameof(EmailConfirmationController.ConfirmEmailSent), typeof(EmailConfirmationController).ControllerName(), new { ReturnUrl = returnUrl }); } if (settings.UsersAreModerated && !user.IsEnabled) @@ -111,63 +112,10 @@ public async Task RegisterPOST(string returnUrl = null) return View(shape); } - [HttpGet] - [AllowAnonymous] - public async Task ConfirmEmail(string userId, string code) - { - if (userId == null || code == null) - { - return RedirectToAction(nameof(Register)); - } - - var user = await _userManager.FindByIdAsync(userId); - - if (user == null) - { - return NotFound(); - } - - var result = await _userManager.ConfirmEmailAsync(user, code); - - if (result.Succeeded) - { - return View(); - } - - return NotFound(); - } - - [HttpGet] - [AllowAnonymous] - public IActionResult ConfirmEmailSent(string returnUrl = null) - => View(new { ReturnUrl = returnUrl }); - - [HttpGet] [AllowAnonymous] public IActionResult RegistrationPending(string returnUrl = null) => View(new { ReturnUrl = returnUrl }); - [Authorize] - [HttpPost] - [ValidateAntiForgeryToken] - public async Task SendVerificationEmail(string id) - { - if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.ManageUsers)) - { - return Forbid(); - } - - var user = await _userManager.FindByIdAsync(id) as User; - if (user != null) - { - await this.SendEmailConfirmationTokenAsync(user, S["Confirm your account"]); - - await _notifier.SuccessAsync(H["Verification email sent."]); - } - - return RedirectToAction(nameof(AdminController.Index), "Admin"); - } - private RedirectResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs index e2fdf6ce8cd..1acf3129dc4 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; -using OrchardCore.Admin; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Liquid; using OrchardCore.Modules; @@ -25,7 +24,8 @@ namespace OrchardCore.Users.Controllers; -[Authorize, Feature(UserConstants.Features.SmsAuthenticator)] +[Authorize] +[Feature(UserConstants.Features.SmsAuthenticator)] public class SmsAuthenticatorController : TwoFactorAuthenticationBaseController { private readonly IdentityOptions _identityOptions; @@ -70,7 +70,6 @@ public SmsAuthenticatorController( _htmlEncoder = htmlEncoder; } - [Admin("Authenticator/Configure/Sms", "ConfigureSmsAuthenticator")] public async Task Index() { var user = await UserManager.GetUserAsync(User); @@ -94,7 +93,8 @@ public async Task Index() return View(model); } - [HttpPost, Admin, ActionName(nameof(Index))] + [HttpPost] + [ActionName(nameof(Index))] public async Task IndexPost(RequestCodeSmsAuthenticatorViewModel model) { var user = await UserManager.GetUserAsync(User); @@ -107,9 +107,9 @@ public async Task IndexPost(RequestCodeSmsAuthenticatorViewModel var currentPhoneNumber = await UserManager.GetPhoneNumberAsync(user); - var canSetNewPhone = settings.AllowChangingPhoneNumber - || string.IsNullOrEmpty(currentPhoneNumber) - || !_phoneFormatValidator.IsValid(currentPhoneNumber); + var canSetNewPhone = settings.AllowChangingPhoneNumber || + string.IsNullOrEmpty(currentPhoneNumber) || + !_phoneFormatValidator.IsValid(currentPhoneNumber); model.AllowChangingPhoneNumber = canSetNewPhone; @@ -147,7 +147,6 @@ public async Task IndexPost(RequestCodeSmsAuthenticatorViewModel return RedirectToAction(nameof(ValidateCode)); } - [Admin("Authenticator/Configure/Sms/ValidateCode", "ConfigureSmsAuthenticatorValidateCode")] public async Task ValidateCode() { var user = await UserManager.GetUserAsync(User); @@ -207,40 +206,6 @@ public async Task ValidateCode(EnableSmsAuthenticatorViewModel mo return View(model); } - [HttpPost, Produces("application/json"), AllowAnonymous] - public async Task SendCode() - { - var user = await SignInManager.GetTwoFactorAuthenticationUserAsync(); - var errorMessage = S["The SMS message could not be sent. Please attempt to request the code at a later time."]; - - if (user == null) - { - return BadRequest(new - { - success = false, - message = errorMessage.Value, - }); - } - - var settings = (await SiteService.GetSiteSettingsAsync()).As(); - var code = await UserManager.GenerateTwoFactorTokenAsync(user, _identityOptions.Tokens.ChangePhoneNumberTokenProvider); - - var message = new SmsMessage() - { - To = await UserManager.GetPhoneNumberAsync(user), - Body = await GetBodyAsync(settings, user, code), - }; - - var result = await _smsService.SendAsync(message); - - return Ok(new - { - success = result.Succeeded, - message = result.Succeeded ? S["A verification code has been sent to your phone number. Please check your device for the code."].Value - : errorMessage.Value, - }); - } - private Task GetBodyAsync(SmsAuthenticatorLoginSettings settings, IUser user, string code) { var message = string.IsNullOrWhiteSpace(settings.Body) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationBaseController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationBaseController.cs index 47cc3242384..f3b8169df39 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationBaseController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationBaseController.cs @@ -79,7 +79,7 @@ protected async Task RemoveTwoFactorProviderAsync(IUser user, Fun if (currentProviders.Count == 1) { - if (await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync()) + if (await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync(user)) { await Notifier.ErrorAsync(H["You cannot remove the only active two-factor method."]); @@ -111,12 +111,14 @@ protected async Task EnableTwoFactorAuthenticationAsync(IUser user) { if (await UserManager.GetTwoFactorEnabledAsync(user)) { + await RefreshTwoFactorClaimAsync(user); + return; } await UserManager.SetTwoFactorEnabledAsync(user, true); - if (await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync()) + if (await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync(user)) { await RefreshTwoFactorClaimAsync(user); } @@ -124,8 +126,7 @@ protected async Task EnableTwoFactorAuthenticationAsync(IUser user) protected async Task RefreshTwoFactorClaimAsync(IUser user) { - var twoFactorClaim = (await UserManager.GetClaimsAsync(user)) - .FirstOrDefault(claim => claim.Type == UserConstants.TwoFactorAuthenticationClaimType); + var twoFactorClaim = User.Claims.FirstOrDefault(claim => claim.Type == UserConstants.TwoFactorAuthenticationClaimType); if (twoFactorClaim != null) { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationController.cs index a1c62245cde..16e4667468b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationController.cs @@ -10,7 +10,6 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using OrchardCore.Admin; using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; @@ -101,7 +100,9 @@ public async Task LoginWithTwoFactorAuthentication(bool rememberM return View(model); } - [HttpPost, AllowAnonymous, ActionName(nameof(LoginWithTwoFactorAuthentication))] + [HttpPost] + [AllowAnonymous] + [ActionName(nameof(LoginWithTwoFactorAuthentication))] public async Task LoginWithTwoFactorAuthenticationPost(LoginWithTwoFactorAuthenticationViewModel model) { var user = await SignInManager.GetTwoFactorAuthenticationUserAsync(); @@ -158,7 +159,6 @@ public async Task LoginWithTwoFactorAuthenticationPost(LoginWithT } [AllowAnonymous] - [Admin(nameof(LoginWithRecoveryCode))] public async Task LoginWithRecoveryCode(string returnUrl = null) { // Ensure the user has gone through the username & password screen first @@ -174,7 +174,8 @@ public async Task LoginWithRecoveryCode(string returnUrl = null) }); } - [HttpPost, AllowAnonymous] + [HttpPost] + [AllowAnonymous] public async Task LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model) { if (ModelState.IsValid) @@ -215,7 +216,6 @@ public async Task LoginWithRecoveryCode(LoginWithRecoveryCodeView return View(model); } - [Admin] public async Task Index() { var user = await UserManager.GetUserAsync(User); @@ -236,7 +236,7 @@ public async Task Index() return View(model); } - [HttpPost, Admin] + [HttpPost] public async Task Index(TwoFactorAuthenticationViewModel model) { var user = await UserManager.GetUserAsync(User); @@ -271,7 +271,7 @@ public async Task Index(TwoFactorAuthenticationViewModel model) return View(model); } - [HttpPost, Admin] + [HttpPost] public async Task ForgetTwoFactorClient() { var user = await UserManager.GetUserAsync(User); @@ -286,7 +286,6 @@ public async Task ForgetTwoFactorClient() return RedirectToAction(nameof(Index)); } - [Admin(nameof(GenerateRecoveryCodes))] public async Task GenerateRecoveryCodes() { var user = await UserManager.GetUserAsync(User); @@ -306,7 +305,8 @@ public async Task GenerateRecoveryCodes() return View(); } - [HttpPost, Admin, ActionName(nameof(GenerateRecoveryCodes))] + [HttpPost] + [ActionName(nameof(GenerateRecoveryCodes))] public async Task GenerateRecoveryCodesPost() { var user = await UserManager.GetUserAsync(User); @@ -331,7 +331,6 @@ public async Task GenerateRecoveryCodesPost() return RedirectToAction(nameof(ShowRecoveryCodes)); } - [Admin(nameof(ShowRecoveryCodes))] public async Task ShowRecoveryCodes() { var user = await UserManager.GetUserAsync(User); @@ -342,7 +341,7 @@ public async Task ShowRecoveryCodes() var userId = await UserManager.GetUserIdAsync(user); - var recoveryCodes = await GetCachedRecoveryCodes(userId); + var recoveryCodes = await GetCachedRecoveryCodesAsync(userId); if (recoveryCodes == null || recoveryCodes.Length == 0) { @@ -356,7 +355,6 @@ public async Task ShowRecoveryCodes() } [HttpPost] - [Admin(nameof(EnableTwoFactorAuthentication))] public async Task EnableTwoFactorAuthentication() { var user = await UserManager.GetUserAsync(User); @@ -381,7 +379,6 @@ public async Task EnableTwoFactorAuthentication() return await RedirectToTwoFactorAsync(user); } - [Admin(nameof(DisableTwoFactorAuthentication))] public async Task DisableTwoFactorAuthentication() { var user = await UserManager.GetUserAsync(User); @@ -390,7 +387,7 @@ public async Task DisableTwoFactorAuthentication() return UserNotFound(); } - if (await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync()) + if (await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync(user)) { await Notifier.WarningAsync(H["Two-factor authentication cannot be disabled for the current user."]); @@ -400,7 +397,8 @@ public async Task DisableTwoFactorAuthentication() return View(); } - [HttpPost, Admin, ActionName(nameof(DisableTwoFactorAuthentication))] + [HttpPost] + [ActionName(nameof(DisableTwoFactorAuthentication))] public async Task DisableTwoFactorAuthenticationPost() { var user = await UserManager.GetUserAsync(User); @@ -409,7 +407,7 @@ public async Task DisableTwoFactorAuthenticationPost() return UserNotFound(); } - if (await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync()) + if (await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync(user)) { await Notifier.WarningAsync(H["Two-factor authentication cannot be disabled for the current user."]); @@ -429,7 +427,7 @@ public async Task DisableTwoFactorAuthenticationPost() return RedirectToAction(nameof(Index)); } - private async Task GetCachedRecoveryCodes(string userId) + private async Task GetCachedRecoveryCodesAsync(string userId) { var key = GetRecoveryCodesCacheKey(userId); @@ -485,7 +483,7 @@ private async Task PopulateModelAsync(IUser user, IList providers, TwoFa model.IsTwoFaEnabled = await UserManager.GetTwoFactorEnabledAsync(user); model.IsMachineRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user); model.RecoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user); - model.CanDisableTwoFactor = !await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync(); + model.CanDisableTwoFactor = !await TwoFactorAuthenticationHandlerCoordinator.IsRequiredAsync(user); model.ValidTwoFactorProviders = providers.Select(providerName => new Microsoft.AspNetCore.Mvc.Rendering.SelectListItem(providerName, providerName)).ToList(); foreach (var key in TwoFactorOptions.Providers) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorLoginSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorLoginSettingsDisplayDriver.cs index c206f456de3..99e81d25805 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorLoginSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorLoginSettingsDisplayDriver.cs @@ -36,12 +36,13 @@ public override IDisplayResult Edit(TwoFactorLoginSettings settings) model.NumberOfRecoveryCodesToGenerate = settings.NumberOfRecoveryCodesToGenerate; model.RequireTwoFactorAuthentication = settings.RequireTwoFactorAuthentication; model.AllowRememberClientTwoFactorAuthentication = settings.AllowRememberClientTwoFactorAuthentication; + model.UseSiteTheme = settings.UseSiteTheme; }).Location("Content:5#Two-Factor Authentication") .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, CommonPermissions.ManageUsers)) .OnGroup(LoginSettingsDisplayDriver.GroupId); } - public override async Task UpdateAsync(TwoFactorLoginSettings section, UpdateEditorContext context) + public override async Task UpdateAsync(TwoFactorLoginSettings settings, UpdateEditorContext context) { if (!context.GroupId.Equals(LoginSettingsDisplayDriver.GroupId, StringComparison.OrdinalIgnoreCase) || !await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, CommonPermissions.ManageUsers)) @@ -49,13 +50,13 @@ public override async Task UpdateAsync(TwoFactorLoginSettings se return null; } - await context.Updater.TryUpdateModelAsync(section, Prefix); + await context.Updater.TryUpdateModelAsync(settings, Prefix); - if (section.NumberOfRecoveryCodesToGenerate < 1) + if (settings.NumberOfRecoveryCodesToGenerate < 1) { - context.Updater.ModelState.AddModelError(Prefix, nameof(section.NumberOfRecoveryCodesToGenerate), S["Number of Recovery Codes to Generate should be grater than 0."]); + context.Updater.ModelState.AddModelError(Prefix, nameof(settings.NumberOfRecoveryCodesToGenerate), S["Number of Recovery Codes to Generate should be grater than 0."]); } - return Edit(section); + return Edit(settings); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs similarity index 90% rename from src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs rename to src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs index 0a0b2ac104f..4e07b7833f0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Extensions/ControllerExtensions.cs @@ -11,6 +11,7 @@ using OrchardCore.Email; using OrchardCore.Environment.Shell; using OrchardCore.Modules; +using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Settings; using OrchardCore.Users.Events; using OrchardCore.Users.Models; @@ -77,7 +78,7 @@ internal static async Task RegisterUser(this Controller controller, Regis UserName = model.UserName, Email = model.Email, EmailConfirmed = !settings.UsersMustValidateEmail, - IsEnabled = !settings.UsersAreModerated + IsEnabled = !settings.UsersAreModerated, }, model.Password, controller.ModelState.AddModelError) as User; if (user != null && controller.ModelState.IsValid) @@ -109,8 +110,19 @@ internal static async Task SendEmailConfirmationTokenAsync(this Controll { var userManager = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService>(); var code = await userManager.GenerateEmailConfirmationTokenAsync(user); - var callbackUrl = controller.Url.Action("ConfirmEmail", "Registration", new { userId = user.UserId, code }, protocol: controller.HttpContext.Request.Scheme); - await SendEmailAsync(controller, user.Email, subject, new ConfirmEmailViewModel() { User = user, ConfirmEmailUrl = callbackUrl }); + var callbackUrl = controller.Url.Action(nameof(EmailConfirmationController.ConfirmEmail), typeof(EmailConfirmationController).ControllerName(), + new + { + userId = user.UserId, + code, + }, + protocol: controller.HttpContext.Request.Scheme); + + await SendEmailAsync(controller, user.Email, subject, new ConfirmEmailViewModel() + { + User = user, + ConfirmEmailUrl = callbackUrl, + }); return callbackUrl; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs b/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs index 89c447af2fe..d98d3d942b1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs @@ -1,58 +1,78 @@ using System; +using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using OrchardCore.Admin; -using OrchardCore.Settings; +using OrchardCore.Mvc.Core.Utilities; +using OrchardCore.Users.Controllers; using OrchardCore.Users.Events; namespace OrchardCore.Users.Filters; public class TwoFactorAuthenticationAuthorizationFilter : IAsyncAuthorizationFilter { + private static readonly string[] _allowedControllerNames = + [ + typeof(EmailConfirmationController).ControllerName(), + typeof(TwoFactorAuthenticationController).ControllerName(), + typeof(AuthenticatorAppController).ControllerName(), + typeof(SmsAuthenticatorController).ControllerName(), + ]; + private readonly UserOptions _userOptions; + private readonly UserManager _userManager; private readonly ITwoFactorAuthenticationHandlerCoordinator _twoFactorHandlerCoordinator; - private readonly AdminOptions _adminOptions; - - private ISiteService _siteService; public TwoFactorAuthenticationAuthorizationFilter( IOptions userOptions, - IOptions adminOptions, + UserManager userManager, ITwoFactorAuthenticationHandlerCoordinator twoFactorHandlerCoordinator) { _userOptions = userOptions.Value; + _userManager = userManager; _twoFactorHandlerCoordinator = twoFactorHandlerCoordinator; - _adminOptions = adminOptions.Value; } public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { ArgumentNullException.ThrowIfNull(context); - if (!(context.HttpContext?.User?.Identity?.IsAuthenticated ?? false) - || context.HttpContext.Request.Path.Equals("/" + _userOptions.LogoffPath, StringComparison.OrdinalIgnoreCase) - || context.HttpContext.Request.Path.Equals("/" + _userOptions.TwoFactorAuthenticationPath, StringComparison.OrdinalIgnoreCase) - || context.HttpContext.Request.Path.StartsWithSegments("/" + _adminOptions.AdminUrlPrefix + "/Authenticator/Configure", StringComparison.OrdinalIgnoreCase) - || context.HttpContext.Request.Path.Equals("/" + _adminOptions.AdminUrlPrefix + "/ShowRecoveryCodes", StringComparison.OrdinalIgnoreCase) - ) + if (context.HttpContext?.User?.Identity?.IsAuthenticated == false || + context.HttpContext.Request.Path.Equals("/" + _userOptions.LogoffPath, StringComparison.OrdinalIgnoreCase) || + context.HttpContext.Request.Path.Equals("/" + _userOptions.TwoFactorAuthenticationPath, StringComparison.OrdinalIgnoreCase) || + context.HttpContext.Request.Path.Equals("/TwoFactor-Authenticator/", StringComparison.OrdinalIgnoreCase)) { return; } - _siteService ??= context.HttpContext.RequestServices.GetService(); + var routeValues = context.HttpContext.Request.RouteValues; + var areaName = routeValues["area"]?.ToString(); - if (_siteService == null) + if (areaName != null && string.Equals(areaName, UserConstants.Features.Users, StringComparison.OrdinalIgnoreCase)) { - return; + var controllerName = routeValues["controller"]?.ToString(); + + if (controllerName != null && _allowedControllerNames.Contains(controllerName, StringComparer.OrdinalIgnoreCase)) + { + return; + } } - if (await _twoFactorHandlerCoordinator.IsRequiredAsync() - && context.HttpContext.User.HasClaim(claim => claim.Type == UserConstants.TwoFactorAuthenticationClaimType)) + if (context.HttpContext.User.HasClaim(claim => claim.Type == UserConstants.TwoFactorAuthenticationClaimType)) { - context.Result = new RedirectResult("~/" + _userOptions.TwoFactorAuthenticationPath); + var user = await _userManager.GetUserAsync(context.HttpContext.User); + + if (user == null) + { + return; + } + + if (await _twoFactorHandlerCoordinator.IsRequiredAsync(user)) + { + context.Result = new RedirectResult("~/" + _userOptions.TwoFactorAuthenticationPath); + } } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs index 9d8b0f00be8..ffc43e988bc 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs @@ -14,19 +14,11 @@ Description = "The users module enables authentication UI and user management.", Dependencies = [ - "OrchardCore.Roles.Core" + "OrchardCore.Roles.Core", ], Category = "Security" )] -[assembly: Feature( - Id = UserConstants.Features.UserEmailConfirmation, - Name = "Users Email Confirmation", - Description = "Provides services to handler user email confirmation.", - Category = "Security", - EnabledByDependencyOnly = true -)] - [assembly: Feature( Id = "OrchardCore.Users.ChangeEmail", Name = "Users Change Email", @@ -34,7 +26,6 @@ Dependencies = [ UserConstants.Features.Users, - UserConstants.Features.UserEmailConfirmation, ], Category = "Security" )] @@ -46,7 +37,6 @@ Dependencies = [ UserConstants.Features.Users, - UserConstants.Features.UserEmailConfirmation, "OrchardCore.Email", ], Category = "Security" @@ -78,7 +68,7 @@ Description = "Provides a way to set the culture per user.", Dependencies = [ - "OrchardCore.Users", + UserConstants.Features.Users, "OrchardCore.Localization" ], Category = "Settings", @@ -146,7 +136,6 @@ [ UserConstants.Features.Users, UserConstants.Features.TwoFactorAuthentication, - UserConstants.Features.UserEmailConfirmation, "OrchardCore.Liquid", "OrchardCore.Email", ], diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/RoleTwoFactorAuthenticationHandler.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/RoleTwoFactorAuthenticationHandler.cs index e415ef8fd23..5154f79ebca 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/RoleTwoFactorAuthenticationHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/RoleTwoFactorAuthenticationHandler.cs @@ -1,5 +1,5 @@ +using System; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using OrchardCore.Settings; using OrchardCore.Users.Events; @@ -9,32 +9,20 @@ namespace OrchardCore.Users.Services; public class RoleTwoFactorAuthenticationHandler : ITwoFactorAuthenticationHandler { - private readonly IHttpContextAccessor _httpContextAccessor; private readonly UserManager _userManager; private readonly ISiteService _siteService; public RoleTwoFactorAuthenticationHandler( - IHttpContextAccessor httpContextAccessor, UserManager userManager, ISiteService siteService) { - _httpContextAccessor = httpContextAccessor; _userManager = userManager; _siteService = siteService; } - public async Task IsRequiredAsync() + public async Task IsRequiredAsync(IUser user) { - if (_httpContextAccessor.HttpContext?.User == null) - { - return false; - } - - var user = await _userManager.GetUserAsync(_httpContextAccessor.HttpContext.User); - if (user == null) - { - return false; - } + ArgumentNullException.ThrowIfNull(user); var loginSettings = (await _siteService.GetSiteSettingsAsync()).As(); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/TwoFactorAuthenticationClaimsProvider.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/TwoFactorAuthenticationClaimsProvider.cs index 3d31a6ba5e1..c24ce1c75af 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/TwoFactorAuthenticationClaimsProvider.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/TwoFactorAuthenticationClaimsProvider.cs @@ -22,11 +22,10 @@ public TwoFactorAuthenticationClaimsProvider( public async Task GenerateAsync(IUser user, ClaimsIdentity claims) { ArgumentNullException.ThrowIfNull(user); - ArgumentNullException.ThrowIfNull(claims); - if (await _twoFactorHandlerCoordinator.IsRequiredAsync() - && !await _userManager.GetTwoFactorEnabledAsync(user)) + if (await _twoFactorHandlerCoordinator.IsRequiredAsync(user) && + !await _userManager.GetTwoFactorEnabledAsync(user)) { // At this point, we know that the user must enable two-factor authentication. claims.AddClaim(new Claim(UserConstants.TwoFactorAuthenticationClaimType, "required")); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs index 999e0f6896a..536c335e20b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs @@ -10,7 +10,8 @@ namespace OrchardCore.Users.Services { /// /// Provides the theme defined in the site configuration for the current scope (request). - /// This selector provides AdminTheme as default or fallback for Account|Registration|ResetPassword + /// This selector provides AdminTheme as default or fallback for Account for Registration, + /// ResetPassword, TwoFactorAuthentication, SmsAuthenticator and AuthenticatorApp /// controllers based on SiteSettings. /// The same is returned if called multiple times /// during the same scope. @@ -35,7 +36,7 @@ public async Task GetThemeAsync() { var routeValues = _httpContextAccessor.HttpContext.Request.RouteValues; - if (routeValues["area"]?.ToString() == "OrchardCore.Users") + if (routeValues["area"]?.ToString() == UserConstants.Features.Users) { bool useSiteTheme; @@ -45,9 +46,24 @@ public async Task GetThemeAsync() useSiteTheme = (await _siteService.GetSiteSettingsAsync()).As().UseSiteTheme; break; case "TwoFactorAuthentication": - useSiteTheme = routeValues["action"] != null - && routeValues["action"].ToString().StartsWith("LoginWith", StringComparison.OrdinalIgnoreCase) - && (await _siteService.GetSiteSettingsAsync()).As().UseSiteTheme; + { + if (routeValues["action"] != null + && routeValues["action"].ToString().StartsWith("LoginWith", StringComparison.OrdinalIgnoreCase) + && (await _siteService.GetSiteSettingsAsync()).As().UseSiteTheme) + { + useSiteTheme = true; + } + else + { + useSiteTheme = (await _siteService.GetSiteSettingsAsync()).As().UseSiteTheme; + } + } + break; + case "SmsAuthenticator": + case "AuthenticatorApp": + { + useSiteTheme = (await _siteService.GetSiteSettingsAsync()).As().UseSiteTheme; + } break; case "Registration": useSiteTheme = (await _siteService.GetSiteSettingsAsync()).As().UseSiteTheme; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index 466bc4db375..d2a1af4de9e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -57,6 +57,8 @@ namespace OrchardCore.Users public sealed class Startup : StartupBase { private static readonly string _accountControllerName = typeof(AccountController).ControllerName(); + private static readonly string _emailConfirmationControllerName = typeof(EmailConfirmationController).ControllerName(); + private readonly string _tenantName; private UserOptions _userOptions; @@ -74,35 +76,77 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde name: "Login", areaName: UserConstants.Features.Users, pattern: _userOptions.LoginPath, - defaults: new { controller = _accountControllerName, action = nameof(AccountController.Login) } + defaults: new + { + controller = _accountControllerName, + action = nameof(AccountController.Login), + } ); routes.MapAreaControllerRoute( name: "ChangePassword", areaName: UserConstants.Features.Users, pattern: _userOptions.ChangePasswordUrl, - defaults: new { controller = _accountControllerName, action = nameof(AccountController.ChangePassword) } + defaults: new + { + controller = _accountControllerName, + action = nameof(AccountController.ChangePassword), + } ); routes.MapAreaControllerRoute( name: "ChangePasswordConfirmation", areaName: UserConstants.Features.Users, pattern: _userOptions.ChangePasswordConfirmationUrl, - defaults: new { controller = _accountControllerName, action = nameof(AccountController.ChangePasswordConfirmation) } + defaults: new + { + controller = _accountControllerName, + action = nameof(AccountController.ChangePasswordConfirmation), + } ); routes.MapAreaControllerRoute( name: "UsersLogOff", areaName: UserConstants.Features.Users, pattern: _userOptions.LogoffPath, - defaults: new { controller = _accountControllerName, action = nameof(AccountController.LogOff) } + defaults: new + { + controller = _accountControllerName, + action = nameof(AccountController.LogOff), + } ); routes.MapAreaControllerRoute( name: "ExternalLogins", areaName: UserConstants.Features.Users, pattern: _userOptions.ExternalLoginsUrl, - defaults: new { controller = _accountControllerName, action = nameof(AccountController.ExternalLogins) } + defaults: new + { + controller = _accountControllerName, + action = nameof(AccountController.ExternalLogins), + } + ); + + routes.MapAreaControllerRoute( + name: "ConfirmEmail", + areaName: UserConstants.Features.Users, + pattern: "ConfirmEmail", + defaults: new + { + controller = _emailConfirmationControllerName, + action = nameof(EmailConfirmationController.ConfirmEmail), + } + ); + + routes.MapAreaControllerRoute( + name: "ConfirmEmailSent", + areaName: UserConstants.Features.Users, + pattern: "ConfirmEmailSent", + defaults: new + { + controller = _emailConfirmationControllerName, + action = nameof(EmailConfirmationController.ConfirmEmailSent), + } ); builder.UseAuthorization(); @@ -123,6 +167,11 @@ public override void ConfigureServices(IServiceCollection services) services.AddIdentity() .AddTokenProvider>(TokenOptions.DefaultProvider); + // Configure token provider for email confirmation. + services.AddTransient, EmailConfirmationIdentityOptionsConfigurations>() + .AddTransient() + .AddOptions(); + services.AddTransient, IdentityOptionsConfigurations>(); services.AddPhoneFormatValidator(); // Configure the authentication options to use the application cookie scheme as the default sign-out handler. @@ -277,17 +326,6 @@ public override void ConfigureServices(IServiceCollection services) } } - [Feature(UserConstants.Features.UserEmailConfirmation)] - public sealed class EmailConfirmationStartup : StartupBase - { - public override void ConfigureServices(IServiceCollection services) - { - services.AddTransient, EmailConfirmationIdentityOptionsConfigurations>() - .AddTransient() - .AddOptions(); - } - } - [Feature("OrchardCore.Users.ChangeEmail")] public sealed class ChangeEmailStartup : StartupBase { @@ -301,14 +339,22 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro name: "ChangeEmail", areaName: UserConstants.Features.Users, pattern: ChangeEmailPath, - defaults: new { controller = ChangeEmailControllerName, action = nameof(ChangeEmailController.Index) } + defaults: new + { + controller = ChangeEmailControllerName, + action = nameof(ChangeEmailController.Index), + } ); routes.MapAreaControllerRoute( name: "ChangeEmailConfirmation", areaName: UserConstants.Features.Users, pattern: ChangeEmailConfirmationPath, - defaults: new { controller = ChangeEmailControllerName, action = nameof(ChangeEmailController.ChangeEmailConfirmation) } + defaults: new + { + controller = ChangeEmailControllerName, + action = nameof(ChangeEmailController.ChangeEmailConfirmation), + } ); } @@ -343,7 +389,7 @@ public override void ConfigureServices(IServiceCollection services) public sealed class RegistrationStartup : StartupBase { private const string RegisterPath = nameof(RegistrationController.Register); - private const string ConfirmEmailSent = nameof(RegistrationController.ConfirmEmailSent); + private const string ConfirmEmailSent = nameof(EmailConfirmationController.ConfirmEmailSent); private const string RegistrationPending = nameof(RegistrationController.RegistrationPending); private const string RegistrationControllerName = "Registration"; @@ -353,21 +399,33 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro name: RegisterPath, areaName: UserConstants.Features.Users, pattern: RegisterPath, - defaults: new { controller = RegistrationControllerName, action = RegisterPath } + defaults: new + { + controller = RegistrationControllerName, + action = RegisterPath, + } ); routes.MapAreaControllerRoute( name: ConfirmEmailSent, areaName: UserConstants.Features.Users, pattern: ConfirmEmailSent, - defaults: new { controller = RegistrationControllerName, action = ConfirmEmailSent } + defaults: new + { + controller = RegistrationControllerName, + action = ConfirmEmailSent, + } ); routes.MapAreaControllerRoute( name: RegistrationPending, areaName: UserConstants.Features.Users, pattern: RegistrationPending, - defaults: new { controller = RegistrationControllerName, action = RegistrationPending } + defaults: new + { + controller = RegistrationControllerName, + action = RegistrationPending, + } ); } @@ -410,25 +468,41 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro name: "ForgotPassword", areaName: UserConstants.Features.Users, pattern: ForgotPasswordPath, - defaults: new { controller = ResetPasswordControllerName, action = nameof(ResetPasswordController.ForgotPassword) } + defaults: new + { + controller = ResetPasswordControllerName, + action = nameof(ResetPasswordController.ForgotPassword), + } ); routes.MapAreaControllerRoute( name: "ForgotPasswordConfirmation", areaName: UserConstants.Features.Users, pattern: ForgotPasswordConfirmationPath, - defaults: new { controller = ResetPasswordControllerName, action = nameof(ResetPasswordController.ForgotPasswordConfirmation) } + defaults: new + { + controller = ResetPasswordControllerName, + action = nameof(ResetPasswordController.ForgotPasswordConfirmation), + } ); routes.MapAreaControllerRoute( name: "ResetPassword", areaName: UserConstants.Features.Users, pattern: ResetPasswordPath, - defaults: new { controller = ResetPasswordControllerName, action = nameof(ResetPasswordController.ResetPassword) } + defaults: new + { + controller = ResetPasswordControllerName, + action = nameof(ResetPasswordController.ResetPassword), + } ); routes.MapAreaControllerRoute( name: "ResetPasswordConfirmation", areaName: UserConstants.Features.Users, pattern: ResetPasswordConfirmationPath, - defaults: new { controller = ResetPasswordControllerName, action = nameof(ResetPasswordController.ResetPasswordConfirmation) } + defaults: new + { + controller = ResetPasswordControllerName, + action = nameof(ResetPasswordController.ResetPasswordConfirmation), + } ); } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs index eb1c2df28b7..154871aac65 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs @@ -65,6 +65,50 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro action = nameof(TwoFactorAuthenticationController.Index), } ); + + routes.MapAreaControllerRoute( + name: "LoginWithRecoveryCode", + areaName: UserConstants.Features.Users, + pattern: "LoginWithRecoveryCode", + defaults: new + { + controller = _twoFactorControllerName, + action = nameof(TwoFactorAuthenticationController.LoginWithRecoveryCode), + } + ); + + routes.MapAreaControllerRoute( + name: "GenerateRecoveryCodes", + areaName: UserConstants.Features.Users, + pattern: "GenerateRecoveryCodes", + defaults: new + { + controller = _twoFactorControllerName, + action = nameof(TwoFactorAuthenticationController.GenerateRecoveryCodes), + } + ); + + routes.MapAreaControllerRoute( + name: "ShowRecoveryCodes", + areaName: UserConstants.Features.Users, + pattern: "ShowRecoveryCodes", + defaults: new + { + controller = _twoFactorControllerName, + action = nameof(TwoFactorAuthenticationController.ShowRecoveryCodes), + } + ); + + routes.MapAreaControllerRoute( + name: "DisableTwoFactorAuthentication", + areaName: UserConstants.Features.Users, + pattern: "DisableTwoFactorAuthentication", + defaults: new + { + controller = _twoFactorControllerName, + action = nameof(TwoFactorAuthenticationController.DisableTwoFactorAuthentication), + } + ); } } @@ -97,6 +141,33 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped, AuthenticatorAppLoginSettingsDisplayDriver>(); services.AddScoped, TwoFactorMethodLoginAuthenticationAppDisplayDriver>(); } + + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + var controllerName = typeof(AuthenticatorAppController).ControllerName(); + + routes.MapAreaControllerRoute( + name: "ConfigureAuthenticatorApp", + areaName: UserConstants.Features.Users, + pattern: "Authenticator/Configure/App", + defaults: new + { + controller = controllerName, + action = nameof(AuthenticatorAppController.Index), + } + ); + + routes.MapAreaControllerRoute( + name: "RemoveAuthenticatorApp", + areaName: UserConstants.Features.Users, + pattern: "Authenticator/Reset/App", + defaults: new + { + controller = controllerName, + action = nameof(AuthenticatorAppController.Reset), + } + ); + } } [Feature(UserConstants.Features.EmailAuthenticator)] diff --git a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EnableEmailAuthenticatorViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EnableEmailAuthenticatorViewModel.cs deleted file mode 100644 index db243d82823..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EnableEmailAuthenticatorViewModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OrchardCore.Users.ViewModels; - -public class EnableEmailAuthenticatorViewModel -{ - [Required] - public string Code { get; set; } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/AuthenticatorApp/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/AuthenticatorApp/Index.cshtml index bb1bd2bcd96..f396ce8a6cf 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/AuthenticatorApp/Index.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/AuthenticatorApp/Index.cshtml @@ -1,6 +1,11 @@ @using OrchardCore.Users.Services + @model EnableAuthenticatorViewModel +@{ + ViewLayout = "Layout__TwoFactor"; +} +