From 0cdf97df962663c24bd4a06488ff76d5985a1a46 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 21 May 2024 15:51:27 -0700 Subject: [PATCH 01/19] Allow any user to manage two-factor --- .../OrchardCore.Admin/AdminFilter.cs | 6 ++- .../OrchardCore.Admin/AdminZoneFilter.cs | 4 +- .../Controllers/AuthenticatorAppController.cs | 12 +++--- .../EmailAuthenticatorController.cs | 19 ++++---- .../Controllers/SmsAuthenticatorController.cs | 12 ++++-- .../TwoFactorAuthenticationBaseController.cs | 6 ++- .../TwoFactorAuthenticationController.cs | 43 +++++++++++-------- ...FactorAuthenticationAuthorizationFilter.cs | 27 +++++++----- .../RoleTwoFactorAuthenticationHandler.cs | 18 ++------ .../TwoFactorAuthenticationClaimsProvider.cs | 5 +-- .../AdminAttribute.cs | 40 ++++++++++++++--- .../Events/ITwoFactorAuthenticationHandler.cs | 4 +- ...oFactorAuthenticationHandlerCoordinator.cs | 2 +- .../DefaultTwoFactorAuthenticationHandler.cs | 2 +- ...oFactorAuthenticationHandlerCoordinator.cs | 4 +- 15 files changed, 124 insertions(+), 80 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Admin/AdminFilter.cs b/src/OrchardCore.Modules/OrchardCore.Admin/AdminFilter.cs index 0dca76e6a94..6130b9966cd 100644 --- a/src/OrchardCore.Modules/OrchardCore.Admin/AdminFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Admin/AdminFilter.cs @@ -25,7 +25,7 @@ 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; } @@ -53,7 +53,9 @@ public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) private Task AuthorizeAsync(Microsoft.AspNetCore.Http.HttpContext context) { - if (AdminAttribute.IsApplied(context)) + var adminAttribute = AdminAttribute.Get(context); + + if (adminAttribute?.RequireAccessAdminPanelPermission == true) { return _authorizationService.AuthorizeAsync(context.User, Permissions.AccessAdminPanel); } diff --git a/src/OrchardCore.Modules/OrchardCore.Admin/AdminZoneFilter.cs b/src/OrchardCore.Modules/OrchardCore.Admin/AdminZoneFilter.cs index 0a57d64825a..34b1fd5ee3a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Admin/AdminZoneFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Admin/AdminZoneFilter.cs @@ -17,14 +17,14 @@ public Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceE { if (action.ControllerName == "Admin") { - AdminAttribute.Apply(context.HttpContext); + AdminAttribute.Apply(context.HttpContext, null); } } else if (context.ActionDescriptor is PageActionDescriptor page) { if (page.ViewEnginePath.Contains("/Admin/")) { - AdminAttribute.Apply(context.HttpContext); + AdminAttribute.Apply(context.HttpContext, null); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs index fe383e00be7..4afdc6212fd 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs @@ -21,7 +21,8 @@ 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}"; @@ -59,7 +60,7 @@ public AuthenticatorAppController( _shellSettings = shellSettings; } - [Admin("Authenticator/Configure/App", "ConfigureAuthenticatorApp")] + [Admin("Authenticator/Configure/App", "ConfigureAuthenticatorApp", false)] public async Task Index(string returnUrl) { var user = await UserManager.GetUserAsync(User); @@ -109,7 +110,7 @@ public async Task Index(EnableAuthenticatorViewModel model) return await RedirectToTwoFactorAsync(user); } - [Admin("Authenticator/Reset/App", "RemoveAuthenticatorApp")] + [Admin("Authenticator/Reset/App", "RemoveAuthenticatorApp", false)] public async Task Reset() { var user = await UserManager.GetUserAsync(User); @@ -122,14 +123,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); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs index 5eb16d5d9ed..510a9cc301f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs @@ -23,7 +23,8 @@ namespace OrchardCore.Users.Controllers; -[Authorize, Feature(UserConstants.Features.EmailAuthenticator)] +[Authorize] +[Feature(UserConstants.Features.EmailAuthenticator)] public class EmailAuthenticatorController : TwoFactorAuthenticationBaseController { private readonly IUserService _userService; @@ -62,7 +63,7 @@ public EmailAuthenticatorController( _htmlEncoder = htmlEncoder; } - [Admin("Authenticator/Configure/Email", "ConfigureEmailAuthenticator")] + [Admin("Authenticator/Configure/Email", "ConfigureEmailAuthenticator", false)] public async Task Index() { var user = await UserManager.GetUserAsync(User); @@ -80,7 +81,7 @@ public async Task Index() } [HttpPost] - [Admin("Authenticator/Configure/Email/RequestCode", "ConfigureEmailAuthenticatorRequestCode")] + [Admin("Authenticator/Configure/Email/RequestCode", "ConfigureEmailAuthenticatorRequestCode", false)] public async Task RequestCode() { var user = await UserManager.GetUserAsync(User); @@ -94,7 +95,7 @@ public async Task RequestCode() return RedirectToTwoFactorIndex(); } - var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + var code = await UserManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider); var settings = (await SiteService.GetSiteSettingsAsync()).As(); @@ -118,7 +119,7 @@ public async Task RequestCode() } [HttpPost] - [Admin("Authenticator/Configure/Email/ValidateCode", "ConfigureEmailAuthenticatorValidateCode")] + [Admin("Authenticator/Configure/Email/ValidateCode", "ConfigureEmailAuthenticatorValidateCode", false)] public async Task ValidateCode(EnableEmailAuthenticatorViewModel model) { var user = await UserManager.GetUserAsync(User); @@ -132,9 +133,9 @@ public async Task ValidateCode(EnableEmailAuthenticatorViewModel return View(model); } - var result = await UserManager.ConfirmEmailAsync(user, StripToken(model.Code)); + var succeeded = await UserManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider, StripToken(model.Code)); - if (result.Succeeded) + if (succeeded) { await EnableTwoFactorAuthenticationAsync(user); @@ -148,7 +149,9 @@ public async Task ValidateCode(EnableEmailAuthenticatorViewModel return View(nameof(RequestCode), model); } - [HttpPost, Produces("application/json"), AllowAnonymous] + [HttpPost] + [Produces("application/json")] + [AllowAnonymous] public async Task SendCode() { var user = await SignInManager.GetTwoFactorAuthenticationUserAsync(); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs index e2fdf6ce8cd..d66c97713d7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs @@ -70,7 +70,7 @@ public SmsAuthenticatorController( _htmlEncoder = htmlEncoder; } - [Admin("Authenticator/Configure/Sms", "ConfigureSmsAuthenticator")] + [Admin("Authenticator/Configure/Sms", "ConfigureSmsAuthenticator", false)] public async Task Index() { var user = await UserManager.GetUserAsync(User); @@ -94,7 +94,9 @@ public async Task Index() return View(model); } - [HttpPost, Admin, ActionName(nameof(Index))] + [HttpPost] + [Admin(requireAccessAdminPanelPermission: false)] + [ActionName(nameof(Index))] public async Task IndexPost(RequestCodeSmsAuthenticatorViewModel model) { var user = await UserManager.GetUserAsync(User); @@ -147,7 +149,7 @@ public async Task IndexPost(RequestCodeSmsAuthenticatorViewModel return RedirectToAction(nameof(ValidateCode)); } - [Admin("Authenticator/Configure/Sms/ValidateCode", "ConfigureSmsAuthenticatorValidateCode")] + [Admin("Authenticator/Configure/Sms/ValidateCode", "ConfigureSmsAuthenticatorValidateCode", false)] public async Task ValidateCode() { var user = await UserManager.GetUserAsync(User); @@ -207,7 +209,9 @@ public async Task ValidateCode(EnableSmsAuthenticatorViewModel mo return View(model); } - [HttpPost, Produces("application/json"), AllowAnonymous] + [HttpPost] + [Produces("application/json")] + [AllowAnonymous] public async Task SendCode() { var user = await SignInManager.GetTwoFactorAuthenticationUserAsync(); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationBaseController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationBaseController.cs index 47cc3242384..d1a6179794b 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); } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationController.cs index a1c62245cde..4c4fe039fc9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationController.cs @@ -101,7 +101,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 +160,7 @@ public async Task LoginWithTwoFactorAuthenticationPost(LoginWithT } [AllowAnonymous] - [Admin(nameof(LoginWithRecoveryCode))] + [Admin(template: nameof(LoginWithRecoveryCode), requireAccessAdminPanelPermission: false)] public async Task LoginWithRecoveryCode(string returnUrl = null) { // Ensure the user has gone through the username & password screen first @@ -174,7 +176,8 @@ public async Task LoginWithRecoveryCode(string returnUrl = null) }); } - [HttpPost, AllowAnonymous] + [HttpPost] + [AllowAnonymous] public async Task LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model) { if (ModelState.IsValid) @@ -215,7 +218,7 @@ public async Task LoginWithRecoveryCode(LoginWithRecoveryCodeView return View(model); } - [Admin] + [Admin(requireAccessAdminPanelPermission: false)] public async Task Index() { var user = await UserManager.GetUserAsync(User); @@ -236,7 +239,8 @@ public async Task Index() return View(model); } - [HttpPost, Admin] + [HttpPost] + [Admin(requireAccessAdminPanelPermission: false)] public async Task Index(TwoFactorAuthenticationViewModel model) { var user = await UserManager.GetUserAsync(User); @@ -271,7 +275,8 @@ public async Task Index(TwoFactorAuthenticationViewModel model) return View(model); } - [HttpPost, Admin] + [HttpPost] + [Admin(requireAccessAdminPanelPermission: false)] public async Task ForgetTwoFactorClient() { var user = await UserManager.GetUserAsync(User); @@ -286,7 +291,7 @@ public async Task ForgetTwoFactorClient() return RedirectToAction(nameof(Index)); } - [Admin(nameof(GenerateRecoveryCodes))] + [Admin(template: nameof(GenerateRecoveryCodes), requireAccessAdminPanelPermission: false)] public async Task GenerateRecoveryCodes() { var user = await UserManager.GetUserAsync(User); @@ -306,7 +311,9 @@ public async Task GenerateRecoveryCodes() return View(); } - [HttpPost, Admin, ActionName(nameof(GenerateRecoveryCodes))] + [HttpPost] + [Admin(requireAccessAdminPanelPermission: false)] + [ActionName(nameof(GenerateRecoveryCodes))] public async Task GenerateRecoveryCodesPost() { var user = await UserManager.GetUserAsync(User); @@ -331,7 +338,7 @@ public async Task GenerateRecoveryCodesPost() return RedirectToAction(nameof(ShowRecoveryCodes)); } - [Admin(nameof(ShowRecoveryCodes))] + [Admin(template: nameof(ShowRecoveryCodes), requireAccessAdminPanelPermission: false)] public async Task ShowRecoveryCodes() { var user = await UserManager.GetUserAsync(User); @@ -342,7 +349,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 +363,7 @@ public async Task ShowRecoveryCodes() } [HttpPost] - [Admin(nameof(EnableTwoFactorAuthentication))] + [Admin(template: nameof(EnableTwoFactorAuthentication), requireAccessAdminPanelPermission: false)] public async Task EnableTwoFactorAuthentication() { var user = await UserManager.GetUserAsync(User); @@ -381,7 +388,7 @@ public async Task EnableTwoFactorAuthentication() return await RedirectToTwoFactorAsync(user); } - [Admin(nameof(DisableTwoFactorAuthentication))] + [Admin(template: nameof(DisableTwoFactorAuthentication), requireAccessAdminPanelPermission: false)] public async Task DisableTwoFactorAuthentication() { var user = await UserManager.GetUserAsync(User); @@ -390,7 +397,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 +407,9 @@ public async Task DisableTwoFactorAuthentication() return View(); } - [HttpPost, Admin, ActionName(nameof(DisableTwoFactorAuthentication))] + [HttpPost] + [Admin(requireAccessAdminPanelPermission: false)] + [ActionName(nameof(DisableTwoFactorAuthentication))] public async Task DisableTwoFactorAuthenticationPost() { var user = await UserManager.GetUserAsync(User); @@ -409,7 +418,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 +438,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 +494,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/Filters/TwoFactorAuthenticationAuthorizationFilter.cs b/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs index 89c447af2fe..3231c93b9f9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs @@ -1,11 +1,10 @@ using System; 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.Users.Events; namespace OrchardCore.Users.Filters; @@ -13,17 +12,18 @@ namespace OrchardCore.Users.Filters; public class TwoFactorAuthenticationAuthorizationFilter : IAsyncAuthorizationFilter { 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; } @@ -42,17 +42,24 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext context) return; } - _siteService ??= context.HttpContext.RequestServices.GetService(); - - if (_siteService == null) + if (context.HttpContext?.User?.Identity?.IsAuthenticated == false) { 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/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/OrchardCore.Admin.Abstractions/AdminAttribute.cs b/src/OrchardCore/OrchardCore.Admin.Abstractions/AdminAttribute.cs index 4cdee6c6332..c86ac76294a 100644 --- a/src/OrchardCore/OrchardCore.Admin.Abstractions/AdminAttribute.cs +++ b/src/OrchardCore/OrchardCore.Admin.Abstractions/AdminAttribute.cs @@ -37,26 +37,54 @@ public class AdminAttribute : Attribute, IAsyncResourceFilter /// public string RouteName { get; set; } - public AdminAttribute(string template = null, string routeName = null) + public bool RequireAccessAdminPanelPermission { get; set; } = true; + + public AdminAttribute(string template = null, string routeName = null, bool requireAccessAdminPanelPermission = true) { Template = template; RouteName = routeName; + RequireAccessAdminPanelPermission = requireAccessAdminPanelPermission; } public Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { - Apply(context.HttpContext); + Apply(context.HttpContext, this); return next(); } - public static void Apply(HttpContext context) + private static readonly string _adminAttributeItemName = typeof(AdminAttribute).Name; + + public static void Apply(HttpContext context, AdminAttribute attributeValue = null) { - // The value isn't important, it's just a marker object - context.Items[typeof(AdminAttribute)] = null; + if (context.Items.TryGetValue(_adminAttributeItemName, out var value) && value is AdminAttribute adminAttribute) + { + if (attributeValue != null) + { + adminAttribute.RequireAccessAdminPanelPermission = attributeValue.RequireAccessAdminPanelPermission; + adminAttribute.Template = attributeValue.Template; + adminAttribute.RouteName = attributeValue.RouteName; + + context.Items[_adminAttributeItemName] = adminAttribute; + } + + return; + } + + context.Items[_adminAttributeItemName] = attributeValue ?? new AdminAttribute(); } public static bool IsApplied(HttpContext context) - => context.Items.ContainsKey(typeof(AdminAttribute)); + => context.Items.ContainsKey(_adminAttributeItemName); + + public static AdminAttribute Get(HttpContext context) + { + if (context.Items.TryGetValue(_adminAttributeItemName, out var value) && value is AdminAttribute adminAttribute) + { + return adminAttribute; + } + + return null; + } } } diff --git a/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandler.cs b/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandler.cs index 07a1fb2e49b..3a584aac265 100644 --- a/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandler.cs +++ b/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandler.cs @@ -5,8 +5,8 @@ namespace OrchardCore.Users.Events; public interface ITwoFactorAuthenticationHandler { /// - /// Checks if the two-factor authentication should be required or not. + /// Checks if the two-factor authentication should be required for the given user or not. /// /// true if the two-factor authentication is required otherwise false. - Task IsRequiredAsync(); + Task IsRequiredAsync(IUser user); } diff --git a/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandlerCoordinator.cs b/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandlerCoordinator.cs index e9021d73ab3..76113a9cf77 100644 --- a/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandlerCoordinator.cs +++ b/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandlerCoordinator.cs @@ -8,5 +8,5 @@ public interface ITwoFactorAuthenticationHandlerCoordinator /// Checks if the two-factor authentication should be required or not. /// /// true if any of the two-factor authentication providers require 2FA otherwise false. - Task IsRequiredAsync(); + Task IsRequiredAsync(IUser user); } diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/DefaultTwoFactorAuthenticationHandler.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/DefaultTwoFactorAuthenticationHandler.cs index 308927a4f22..aa9db239c36 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/DefaultTwoFactorAuthenticationHandler.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/DefaultTwoFactorAuthenticationHandler.cs @@ -14,7 +14,7 @@ public DefaultTwoFactorAuthenticationHandler(ISiteService siteService) _siteService = siteService; } - public async Task IsRequiredAsync() + public async Task IsRequiredAsync(IUser user) { return (await _siteService.GetSiteSettingsAsync()).As().RequireTwoFactorAuthentication; } diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/DefaultTwoFactorAuthenticationHandlerCoordinator.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/DefaultTwoFactorAuthenticationHandlerCoordinator.cs index d40f617b845..15a95737456 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/DefaultTwoFactorAuthenticationHandlerCoordinator.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/DefaultTwoFactorAuthenticationHandlerCoordinator.cs @@ -16,13 +16,13 @@ public DefaultTwoFactorAuthenticationHandlerCoordinator( _twoFactorAuthenticationHandlers = twoFactorAuthenticationHandlers; } - public async Task IsRequiredAsync() + public async Task IsRequiredAsync(IUser user) { if (_isRequired is null) { foreach (var handler in _twoFactorAuthenticationHandlers) { - if (await handler.IsRequiredAsync()) + if (await handler.IsRequiredAsync(user)) { _isRequired = true; From f82fe052ccff1f0969292805e4cf5cc66b4dde51 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Wed, 22 May 2024 08:59:52 -0700 Subject: [PATCH 02/19] Use email confirmation for 2FA --- .../Controllers/UserPickerAdminController.cs | 2 +- .../OrchardCore.ContentFields/Manifest.cs | 7 +- .../OrchardCore.ContentFields/Startup.cs | 3 +- .../OrchardCore.Demo/Manifest.cs | 7 +- .../Controllers/AdminController.cs | 8 +- .../OrchardCore.Notifications/Startup.cs | 3 +- .../OrchardCore.ReCaptcha/Manifest.cs | 7 +- .../OrchardCore.Users/AdminMenu.cs | 2 +- .../Controllers/AccountController.cs | 22 ++-- .../Controllers/ControllerExtensions.cs | 10 +- .../EmailAuthenticatorController.cs | 77 +------------ .../EmailConfirmationController.cs | 107 ++++++++++++++++++ .../Controllers/RegistrationController.cs | 56 +-------- .../TwoFactorAuthenticationBaseController.cs | 3 +- ...FactorAuthenticationAuthorizationFilter.cs | 8 +- .../OrchardCore.Users/Manifest.cs | 15 +-- .../Services/UsersThemeSelector.cs | 2 +- .../OrchardCore.Users/Startup.cs | 18 +-- .../ConfirmEmail.cshtml | 0 .../ConfirmEmailSent.cshtml | 0 .../TwoFactorMethod.Email.Actions.cshtml | 16 ++- .../ResetPasswordConfirmation.cshtml | 2 +- .../Views/TemplateUserConfirmEmail.cshtml | 3 +- .../Views/TemplateUserLostPassword.cshtml | 3 +- .../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 +- .../OrchardCore.Users.Core/UserConstants.cs | 2 - 32 files changed, 197 insertions(+), 204 deletions(-) create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailConfirmationController.cs rename src/OrchardCore.Modules/OrchardCore.Users/Views/{Registration => EmailConfirmation}/ConfirmEmail.cshtml (100%) rename src/OrchardCore.Modules/OrchardCore.Users/Views/{Registration => EmailConfirmation}/ConfirmEmailSent.cshtml (100%) 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..69e5b59df36 100644 --- a/src/OrchardCore.Modules/OrchardCore.Notifications/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Notifications/Controllers/AdminController.cs @@ -71,7 +71,7 @@ public AdminController( H = htmlLocalizer; } - [Admin("notifications", "ListNotifications")] + [Admin("notifications", "ListNotifications", requireAccessAdminPanelPermission: false)] public async Task List( [ModelBinder(BinderType = typeof(NotificationFilterEngineModelBinder), Name = "q")] QueryFilterResult queryFilterResult, PagerParameters pagerParameters, @@ -143,7 +143,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 +163,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 c53d0cf8b37..a38d9dd1831 100644 --- a/src/OrchardCore.Modules/OrchardCore.Notifications/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Notifications/Startup.cs @@ -16,6 +16,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; @@ -77,7 +78,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.ReCaptcha/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Manifest.cs index 4d091b73954..962b7327fd1 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 03d6e4a9ce5..94a7e5f65de 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs @@ -37,7 +37,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/ControllerExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs index 0a0b2ac104f..5b108c79c8c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/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; @@ -109,7 +110,14 @@ 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); + 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/Controllers/EmailAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs index 510a9cc301f..d80a3b98d05 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs @@ -18,8 +18,6 @@ using OrchardCore.Settings; using OrchardCore.Users.Events; using OrchardCore.Users.Models; -using OrchardCore.Users.Services; -using OrchardCore.Users.ViewModels; namespace OrchardCore.Users.Controllers; @@ -27,7 +25,6 @@ namespace OrchardCore.Users.Controllers; [Feature(UserConstants.Features.EmailAuthenticator)] public class EmailAuthenticatorController : TwoFactorAuthenticationBaseController { - private readonly IUserService _userService; private readonly IEmailService _emailService; private readonly ILiquidTemplateManager _liquidTemplateManager; private readonly HtmlEncoder _htmlEncoder; @@ -41,7 +38,6 @@ public EmailAuthenticatorController( IOptions twoFactorOptions, INotifier notifier, IDistributedCache distributedCache, - IUserService userService, IEmailService emailService, ILiquidTemplateManager liquidTemplateManager, HtmlEncoder htmlEncoder, @@ -57,7 +53,6 @@ public EmailAuthenticatorController( stringLocalizer, twoFactorOptions) { - _userService = userService; _emailService = emailService; _liquidTemplateManager = liquidTemplateManager; _htmlEncoder = htmlEncoder; @@ -80,75 +75,6 @@ public async Task Index() return View(); } - [HttpPost] - [Admin("Authenticator/Configure/Email/RequestCode", "ConfigureEmailAuthenticatorRequestCode", false)] - 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.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider); - - 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", false)] - public async Task ValidateCode(EnableEmailAuthenticatorViewModel model) - { - var user = await UserManager.GetUserAsync(User); - if (user == null) - { - return UserNotFound(); - } - - if (!ModelState.IsValid) - { - return View(model); - } - - var succeeded = await UserManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider, StripToken(model.Code)); - - if (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); - } - [HttpPost] [Produces("application/json")] [AllowAnonymous] @@ -177,7 +103,8 @@ public async Task SendCode() return Ok(new { success = result.Succeeded, - message = result.Succeeded ? S["A verification code has been sent via email. Please check your email for the code."].Value + message = result.Succeeded + ? S["A verification code has been sent via email. Please check your email for the code."].Value : errorMessage.Value, }); } 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/TwoFactorAuthenticationBaseController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationBaseController.cs index d1a6179794b..f3b8169df39 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationBaseController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationBaseController.cs @@ -126,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/Filters/TwoFactorAuthenticationAuthorizationFilter.cs b/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs index 3231c93b9f9..f519a0b4dda 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs @@ -32,21 +32,17 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { ArgumentNullException.ThrowIfNull(context); - if (!(context.HttpContext?.User?.Identity?.IsAuthenticated ?? false) + 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 + "/EnableTwoFactorAuthentication", StringComparison.OrdinalIgnoreCase) || context.HttpContext.Request.Path.Equals("/" + _adminOptions.AdminUrlPrefix + "/ShowRecoveryCodes", StringComparison.OrdinalIgnoreCase) ) { return; } - if (context.HttpContext?.User?.Identity?.IsAuthenticated == false) - { - return; - } - if (context.HttpContext.User.HasClaim(claim => claim.Type == UserConstants.TwoFactorAuthenticationClaimType)) { var user = await _userManager.GetUserAsync(context.HttpContext.User); 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/UsersThemeSelector.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs index 999e0f6896a..9890aa630ae 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs @@ -35,7 +35,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; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index 466bc4db375..22dbbc6bf65 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -123,6 +123,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 +282,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 { @@ -343,7 +337,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"; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Registration/ConfirmEmail.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/EmailConfirmation/ConfirmEmail.cshtml similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/Registration/ConfirmEmail.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/EmailConfirmation/ConfirmEmail.cshtml diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Registration/ConfirmEmailSent.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/EmailConfirmation/ConfirmEmailSent.cshtml similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/Registration/ConfirmEmailSent.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/EmailConfirmation/ConfirmEmailSent.cshtml diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Actions.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Actions.cshtml index d70b92b5478..cd5bf9293dd 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Actions.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Actions.cshtml @@ -1,12 +1,18 @@ @using OrchardCore.Users.Models +@using System.Security.Claims @model TwoFactorMethod @if (!Model.IsEnabled) { - @T["Verify Email"] -} -else -{ -
@T["Verified"]
+
+
+

@T["Your email is not yet verified. To use email for two-factor authentication, you must first verify it."]

+ +
+
+ + return; } + +
@T["Verified"]
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ResetPasswordConfirmation.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ResetPasswordConfirmation.cshtml index e291c9b9ca3..f41e4de0810 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ResetPasswordConfirmation.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ResetPasswordConfirmation.cshtml @@ -5,5 +5,5 @@

@T["Reset Password"]

@T["Your password has been reset."] - @T["Please "]@T["Click here to log in"] + @T["Please "]@T["Click here to log in"]

diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserConfirmEmail.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserConfirmEmail.cshtml index c40ba31fdca..dbad46ccb69 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserConfirmEmail.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserConfirmEmail.cshtml @@ -6,6 +6,7 @@ @{ Layout = string.Empty; + var totalMinutes = Convert.ToInt32(TokenOptions.Value.TokenLifespan.TotalMinutes); }

@@ -14,6 +15,6 @@

@T["Please click here to confirm your account.", Model.ConfirmEmailUrl]

-

@T.Plural(TokenOptions.Value.TokenLifespan.Minutes, "Please be aware that this link will expire in {0} minute.", "Please be aware that this link will expire in {0} minutes.", TokenOptions.Value.TokenLifespan.Minutes)

+

@T.Plural(totalMinutes, "Please be aware that this link will expire in {0} minute.", "Please be aware that this link will expire in {0} minutes.", totalMinutes)

@T["Thank you"]

diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserLostPassword.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserLostPassword.cshtml index 503138297e5..f541471062d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserLostPassword.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserLostPassword.cshtml @@ -4,6 +4,7 @@ @inject IOptions TokenOptions @{ Layout = string.Empty; + var totalMinutes = Convert.ToInt32(TokenOptions.Value.TokenLifespan.TotalMinutes); }

@T["Someone recently requested that the password be reset for {0}.", Model.User.UserName]

@@ -12,4 +13,4 @@

@T["If you did not request a password reset please ignore this email - your password will not be changed."]

-

@T.Plural(TokenOptions.Value.TokenLifespan.Minutes, "Please be aware that this link will expire in {0} minute.", "Please be aware that this link will expire in {0} minutes.", TokenOptions.Value.TokenLifespan.Minutes)

+

@T.Plural(totalMinutes, "Please be aware that this link will expire in {0} minute.", "Please be aware that this link will expire in {0} minutes.", totalMinutes)

diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenu.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenu.cshtml index faa84431a0f..84480e3d156 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenu.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenu.cshtml @@ -23,7 +23,7 @@ } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ChangeEmail.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ChangeEmail.cshtml index 6c255036b7f..cf682c25896 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ChangeEmail.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ChangeEmail.cshtml @@ -1,5 +1,5 @@
  • - + @T["Change Email"]
  • diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ExternalLogins.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ExternalLogins.cshtml index 40fd34651fe..f52842f9684 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ExternalLogins.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-ExternalLogins.cshtml @@ -1,5 +1,5 @@
  • - + @T["External Logins"]
  • diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-Profile.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-Profile.cshtml index 370accd50b2..4a789cf4634 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-Profile.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-Profile.cshtml @@ -5,7 +5,7 @@ @if (await AuthorizationService.AuthorizeAsync(User, CommonPermissions.EditOwnUser)) {
  • - + @T["Profile"]
  • @@ -13,7 +13,7 @@ else if (await AuthorizationService.AuthorizeAsync(User, CommonPermissions.ViewUsers)) {
  • - + @T["Profile"]
  • diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-SignOut.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-SignOut.cshtml index 0d87680df96..9af81ae8a4d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-SignOut.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-SignOut.cshtml @@ -1,10 +1,10 @@
  • - + @T["Change password"]
  • -
    + diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-TwoFactor.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-TwoFactor.cshtml index f0092601673..d9cbe1fda7f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-TwoFactor.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenuItems-TwoFactor.cshtml @@ -1,5 +1,5 @@
  • - + @T["Security"]
  • diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs b/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs index bb456774356..16328fc361f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs @@ -128,7 +128,7 @@ public override async Task ExecuteAsync(WorkflowExecuti var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var uri = _linkGenerator.GetUriByAction(_httpContextAccessor.HttpContext, "ConfirmEmail", - "Registration", new { area = "OrchardCore.Users", userId = user.UserId, code }); + "Registration", new { area = UserConstants.Features.Users, userId = user.UserId, code }); workflowContext.Properties["EmailConfirmationUrl"] = uri; diff --git a/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs b/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs index a4692395ea0..ca3aa1e63e0 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs @@ -16,8 +16,6 @@ public class Features public const string SmsAuthenticator = "OrchardCore.Users.2FA.Sms"; - public const string UserEmailConfirmation = "OrchardCore.Users.EmailConfirmation"; - public const string UserRegistration = "OrchardCore.Users.Registration"; public const string ResetPassword = "OrchardCore.Users.ResetPassword"; From 84be6414629fcc924bb005205d5da4827a64d1bb Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Wed, 22 May 2024 09:05:40 -0700 Subject: [PATCH 03/19] cleanup --- src/OrchardCore.Modules/OrchardCore.ReCaptcha/Manifest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Manifest.cs index 962b7327fd1..9bd65b5e6a6 100644 --- a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Manifest.cs @@ -21,5 +21,5 @@ Dependencies = [ "OrchardCore.ReCaptcha", - UserConstants.Features.Users + UserConstants.Features.Users, ])] From aa62415c5e8991bae827c5bb83d561ba6b0e7c5d Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Wed, 22 May 2024 09:08:43 -0700 Subject: [PATCH 04/19] document the breaking change --- src/docs/releases/2.0.0.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/docs/releases/2.0.0.md b/src/docs/releases/2.0.0.md index 5a1f1bfd675..b97346e79ce 100644 --- a/src/docs/releases/2.0.0.md +++ b/src/docs/releases/2.0.0.md @@ -195,6 +195,8 @@ public class RegisterUserFormDisplayDriver : DisplayDriver } ``` +Lastly, The method `IsRequiredAsync()` in both the `ITwoFactorAuthenticationHandlerCoordinator` and `ITwoFactorAuthenticationHandler` was changed to `IsRequiredAsync(IUser user)`. + ### Contents The `IContentManager` interface was modified. The method `Task> GetAsync(IEnumerable contentItemIds, bool latest = false)` was removed. Instead use the method that accepts `VersionOptions` by providing either `VersionOptions.Latest` or `VersionOptions.Published` will be used by default. From a0ea4fd9db9804512099f414ac1a3480f9e67e5f Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Wed, 22 May 2024 09:30:23 -0700 Subject: [PATCH 05/19] Fix tests --- .../OrchardCore.Users/Startup.cs | 16 ++++++++++++++++ .../RegistrationControllerTests.cs | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index 22dbbc6bf65..64ec7e11316 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; @@ -105,6 +107,20 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde 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(); } diff --git a/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs b/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs index fba421b4481..0b76d301d73 100644 --- a/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs +++ b/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs @@ -251,7 +251,7 @@ public async Task Register_WhenRequireEmailConfirmation_RedirectToConfirmEmailSe // Assert Assert.Equal(HttpStatusCode.Redirect, responseFromPost.StatusCode); - Assert.Equal($"/{context.TenantName}/{nameof(RegistrationController.ConfirmEmailSent)}", responseFromPost.Headers.Location.ToString()); + Assert.Equal($"/{context.TenantName}/ConfirmEmailSent", responseFromPost.Headers.Location.ToString()); await context.UsingTenantScopeAsync(async scope => { From c15e1d5aef69beea0163803df24eca06049115f8 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Thu, 23 May 2024 17:39:29 -0700 Subject: [PATCH 06/19] address feedback --- .../ViewModels/EnableEmailAuthenticatorViewModel.cs | 9 --------- .../OrchardCore.Admin.Abstractions/AdminAttribute.cs | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EnableEmailAuthenticatorViewModel.cs 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/OrchardCore.Admin.Abstractions/AdminAttribute.cs b/src/OrchardCore/OrchardCore.Admin.Abstractions/AdminAttribute.cs index c86ac76294a..69d32318777 100644 --- a/src/OrchardCore/OrchardCore.Admin.Abstractions/AdminAttribute.cs +++ b/src/OrchardCore/OrchardCore.Admin.Abstractions/AdminAttribute.cs @@ -14,6 +14,8 @@ namespace OrchardCore.Admin [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class AdminAttribute : Attribute, IAsyncResourceFilter { + private static readonly string _adminAttributeItemName = typeof(AdminAttribute).Name; + /// /// This may be used if the route name should be just the controller and action names stuck together. For /// example ~/Admin/AdminMenu/List gets the route name AdminMenuList. @@ -53,8 +55,6 @@ public Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceE return next(); } - private static readonly string _adminAttributeItemName = typeof(AdminAttribute).Name; - public static void Apply(HttpContext context, AdminAttribute attributeValue = null) { if (context.Items.TryGetValue(_adminAttributeItemName, out var value) && value is AdminAttribute adminAttribute) From 5b91f725c1d6b541e632bddc8550d9b3fea854f7 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Thu, 23 May 2024 19:11:06 -0700 Subject: [PATCH 07/19] Fix build --- .../OrchardCore.Admin/AdminZoneFilter.cs | 4 ++-- .../Views/EmailAuthenticator/Index.cshtml | 17 ---------------- .../EmailAuthenticator/RequestCode.cshtml | 20 ------------------- 3 files changed, 2 insertions(+), 39 deletions(-) delete mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/EmailAuthenticator/Index.cshtml delete mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/EmailAuthenticator/RequestCode.cshtml diff --git a/src/OrchardCore.Modules/OrchardCore.Admin/AdminZoneFilter.cs b/src/OrchardCore.Modules/OrchardCore.Admin/AdminZoneFilter.cs index 34b1fd5ee3a..0a57d64825a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Admin/AdminZoneFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Admin/AdminZoneFilter.cs @@ -17,14 +17,14 @@ public Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceE { if (action.ControllerName == "Admin") { - AdminAttribute.Apply(context.HttpContext, null); + AdminAttribute.Apply(context.HttpContext); } } else if (context.ActionDescriptor is PageActionDescriptor page) { if (page.ViewEnginePath.Contains("/Admin/")) { - AdminAttribute.Apply(context.HttpContext, null); + AdminAttribute.Apply(context.HttpContext); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/EmailAuthenticator/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/EmailAuthenticator/Index.cshtml deleted file mode 100644 index 8956e934d12..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/EmailAuthenticator/Index.cshtml +++ /dev/null @@ -1,17 +0,0 @@ -@using OrchardCore.Users.Services -@model EnableEmailAuthenticatorViewModel - - - -
    - @T["To utilize email as a two-factor authentication method, please click the 'Request Code' button below in order to confirm your email address."] -
    - - - - diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/EmailAuthenticator/RequestCode.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/EmailAuthenticator/RequestCode.cshtml deleted file mode 100644 index 0219148c5d9..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/EmailAuthenticator/RequestCode.cshtml +++ /dev/null @@ -1,20 +0,0 @@ -@model EnableEmailAuthenticatorViewModel - - - - - -
    -
    - - - -
    - -
    From 4415a6901e08e731104bdb43bc41c872b17b040e Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Thu, 23 May 2024 19:31:37 -0700 Subject: [PATCH 08/19] docs --- .../Events/ITwoFactorAuthenticationHandler.cs | 1 + .../Events/ITwoFactorAuthenticationHandlerCoordinator.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandler.cs b/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandler.cs index 3a584aac265..e1cf54959da 100644 --- a/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandler.cs +++ b/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandler.cs @@ -7,6 +7,7 @@ public interface ITwoFactorAuthenticationHandler /// /// Checks if the two-factor authentication should be required for the given user or not. /// + /// An instance of the user to evaluate. /// true if the two-factor authentication is required otherwise false. Task IsRequiredAsync(IUser user); } diff --git a/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandlerCoordinator.cs b/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandlerCoordinator.cs index 76113a9cf77..f30b594c423 100644 --- a/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandlerCoordinator.cs +++ b/src/OrchardCore/OrchardCore.Users.Abstractions/Events/ITwoFactorAuthenticationHandlerCoordinator.cs @@ -7,6 +7,7 @@ public interface ITwoFactorAuthenticationHandlerCoordinator /// /// Checks if the two-factor authentication should be required or not. /// + /// An instance of the user to evaluate. /// true if any of the two-factor authentication providers require 2FA otherwise false. Task IsRequiredAsync(IUser user); } From a8810a6f43cfc563024a7f6b8fe75e32b150af4b Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 24 May 2024 14:14:37 -0700 Subject: [PATCH 09/19] Use Layout-TwoFactor instead of changing the AdminAttribute --- .../OrchardCore.Admin/AdminFilter.cs | 8 +- .../{Permissions.cs => PermissionProvider.cs} | 6 +- .../OrchardCore.Admin/Startup.cs | 2 +- .../Controllers/AdminController.cs | 3 +- .../Controllers/AdminController.cs | 3 +- .../Views/UserNotificationNavbar.cshtml | 12 ++- .../Controllers/AuthenticatorAppController.cs | 7 +- .../EmailAuthenticatorController.cs | 3 +- .../Controllers/SmsAuthenticatorController.cs | 8 +- .../TwoFactorAuthenticationController.cs | 11 --- ...FactorAuthenticationAuthorizationFilter.cs | 39 +++++++--- .../Services/UsersThemeSelector.cs | 21 ++++- .../TwoFactorAuthenticationStartup.cs | 78 +++++++++++++++++++ .../Views/AuthenticatorApp/Index.cshtml | 9 ++- .../Views/AuthenticatorApp/Reset.cshtml | 4 + .../TwoFactorMethod.Email.Actions.cshtml | 2 +- .../Views/SmsAuthenticator/Index.cshtml | 5 ++ .../SmsAuthenticator/ValidateCode.cshtml | 5 ++ .../DisableTwoFactorAuthentication.cshtml | 4 + .../GenerateRecoveryCodes.cshtml | 4 + .../TwoFactorAuthentication/Index.cshtml | 2 +- .../LoginWithRecoveryCode.cshtml | 2 +- .../LoginWithTwoFactorAuthentication.cshtml | 7 +- .../ShowRecoveryCodes.cshtml | 4 + .../Views/TwoFactorLoginSettings.Edit.cshtml | 10 +++ .../TheAdmin/Views/Layout-TwoFactor.cshtml | 53 +++++++++++++ .../Views/UserNotificationNavbar.cshtml | 12 ++- .../AdminAttribute.cs | 40 ++-------- .../Permissions.cs | 8 ++ .../Models/TwoFactorLoginSettings.cs | 2 + 30 files changed, 276 insertions(+), 98 deletions(-) rename src/OrchardCore.Modules/OrchardCore.Admin/{Permissions.cs => PermissionProvider.cs} (86%) create mode 100644 src/OrchardCore.Themes/TheAdmin/Views/Layout-TwoFactor.cshtml create mode 100644 src/OrchardCore/OrchardCore.Admin.Abstractions/Permissions.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Admin/AdminFilter.cs b/src/OrchardCore.Modules/OrchardCore.Admin/AdminFilter.cs index 6130b9966cd..c756de9e471 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 ? new ForbidResult() : new ChallengeResult(); + context.Result = context.HttpContext.User?.Identity?.IsAuthenticated ?? false + ? new ForbidResult() + : new ChallengeResult(); return; } @@ -53,9 +55,7 @@ public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) private Task AuthorizeAsync(Microsoft.AspNetCore.Http.HttpContext context) { - var adminAttribute = AdminAttribute.Get(context); - - if (adminAttribute?.RequireAccessAdminPanelPermission == true) + if (AdminAttribute.IsApplied(context)) { return _authorizationService.AuthorizeAsync(context.User, Permissions.AccessAdminPanel); } diff --git a/src/OrchardCore.Modules/OrchardCore.Admin/Permissions.cs b/src/OrchardCore.Modules/OrchardCore.Admin/PermissionProvider.cs similarity index 86% rename from src/OrchardCore.Modules/OrchardCore.Admin/Permissions.cs rename to src/OrchardCore.Modules/OrchardCore.Admin/PermissionProvider.cs index 334dfb7222d..cbfced8986f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Admin/Permissions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Admin/PermissionProvider.cs @@ -4,13 +4,11 @@ namespace OrchardCore.Admin; -public class Permissions : IPermissionProvider +public class PermissionProvider : IPermissionProvider { - public static readonly Permission AccessAdminPanel = new("AccessAdminPanel", "Access admin panel"); - private readonly IEnumerable _allPermissions = [ - AccessAdminPanel, + Permissions.AccessAdminPanel, ]; public Task> GetPermissionsAsync() diff --git a/src/OrchardCore.Modules/OrchardCore.Admin/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Admin/Startup.cs index 41178b4f5ad..ec26cc9e3dc 100644 --- a/src/OrchardCore.Modules/OrchardCore.Admin/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Admin/Startup.cs @@ -53,7 +53,7 @@ public override void ConfigureServices(IServiceCollection services) }); services.AddTransient(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped, AdminSiteSettingsDisplayDriver>(); diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Email/Controllers/AdminController.cs index 51e4120a113..af673ccd5fc 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Email/Controllers/AdminController.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; -using OrchardCore.Admin; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Email.Core; using OrchardCore.Email.Core.Services; @@ -48,7 +47,7 @@ public AdminController( S = stringLocalizer; } - [Admin("Email/Test", "EmailTest")] + [Admin.Admin("Email/Test", "EmailTest")] public async Task Test() { if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageEmailSettings)) diff --git a/src/OrchardCore.Modules/OrchardCore.Notifications/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Notifications/Controllers/AdminController.cs index 69e5b59df36..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, @@ -71,7 +70,7 @@ public AdminController( H = htmlLocalizer; } - [Admin("notifications", "ListNotifications", requireAccessAdminPanelPermission: false)] + [Admin("notifications", "ListNotifications")] public async Task List( [ModelBinder(BinderType = typeof(NotificationFilterEngineModelBinder), Name = "q")] QueryFilterResult queryFilterResult, PagerParameters pagerParameters, diff --git a/src/OrchardCore.Modules/OrchardCore.Notifications/Views/UserNotificationNavbar.cshtml b/src/OrchardCore.Modules/OrchardCore.Notifications/Views/UserNotificationNavbar.cshtml index dbe75afa2c2..d639a880e2e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Notifications/Views/UserNotificationNavbar.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Notifications/Views/UserNotificationNavbar.cshtml @@ -1,8 +1,11 @@ @using Microsoft.AspNetCore.Mvc.Localization @using OrchardCore.DisplayManagement.ModelBinding +@using Microsoft.AspNetCore.Authorization +@using OrchardCore.Admin @inject IDisplayManager NotificationDisplayDriver @inject IUpdateModelAccessor UpdateModelAccessor +@inject IAuthorizationService AuthorizationService @model UserNotificationNavbarViewModel @@ -48,9 +51,12 @@
  • } -
  • - @T["Notification Center"] -
  • + @if (await AuthorizationService.AuthorizeAsync(ViewContext.HttpContext.User, Permissions.AccessAdminPanel)) + { +
  • + @T["Notification Center"] +
  • + } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs index 4afdc6212fd..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; @@ -25,7 +24,7 @@ namespace OrchardCore.Users.Controllers; [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; @@ -60,7 +59,6 @@ public AuthenticatorAppController( _shellSettings = shellSettings; } - [Admin("Authenticator/Configure/App", "ConfigureAuthenticatorApp", false)] public async Task Index(string returnUrl) { var user = await UserManager.GetUserAsync(User); @@ -110,7 +108,6 @@ public async Task Index(EnableAuthenticatorViewModel model) return await RedirectToTwoFactorAsync(user); } - [Admin("Authenticator/Reset/App", "RemoveAuthenticatorApp", false)] public async Task Reset() { var user = await UserManager.GetUserAsync(User); @@ -197,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 index d80a3b98d05..79b019bd36e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.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.Email; using OrchardCore.Liquid; @@ -58,7 +57,6 @@ public EmailAuthenticatorController( _htmlEncoder = htmlEncoder; } - [Admin("Authenticator/Configure/Email", "ConfigureEmailAuthenticator", false)] public async Task Index() { var user = await UserManager.GetUserAsync(User); @@ -75,6 +73,7 @@ public async Task Index() return View(); } + // TODO: move this action into minimal API. [HttpPost] [Produces("application/json")] [AllowAnonymous] diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs index d66c97713d7..1c402d62d7c 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", false)] public async Task Index() { var user = await UserManager.GetUserAsync(User); @@ -95,7 +94,6 @@ public async Task Index() } [HttpPost] - [Admin(requireAccessAdminPanelPermission: false)] [ActionName(nameof(Index))] public async Task IndexPost(RequestCodeSmsAuthenticatorViewModel model) { @@ -149,7 +147,6 @@ public async Task IndexPost(RequestCodeSmsAuthenticatorViewModel return RedirectToAction(nameof(ValidateCode)); } - [Admin("Authenticator/Configure/Sms/ValidateCode", "ConfigureSmsAuthenticatorValidateCode", false)] public async Task ValidateCode() { var user = await UserManager.GetUserAsync(User); @@ -209,6 +206,7 @@ public async Task ValidateCode(EnableSmsAuthenticatorViewModel mo return View(model); } + // TODO: move this action into minimal API. [HttpPost] [Produces("application/json")] [AllowAnonymous] diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/TwoFactorAuthenticationController.cs index 4c4fe039fc9..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; @@ -160,7 +159,6 @@ public async Task LoginWithTwoFactorAuthenticationPost(LoginWithT } [AllowAnonymous] - [Admin(template: nameof(LoginWithRecoveryCode), requireAccessAdminPanelPermission: false)] public async Task LoginWithRecoveryCode(string returnUrl = null) { // Ensure the user has gone through the username & password screen first @@ -218,7 +216,6 @@ public async Task LoginWithRecoveryCode(LoginWithRecoveryCodeView return View(model); } - [Admin(requireAccessAdminPanelPermission: false)] public async Task Index() { var user = await UserManager.GetUserAsync(User); @@ -240,7 +237,6 @@ public async Task Index() } [HttpPost] - [Admin(requireAccessAdminPanelPermission: false)] public async Task Index(TwoFactorAuthenticationViewModel model) { var user = await UserManager.GetUserAsync(User); @@ -276,7 +272,6 @@ public async Task Index(TwoFactorAuthenticationViewModel model) } [HttpPost] - [Admin(requireAccessAdminPanelPermission: false)] public async Task ForgetTwoFactorClient() { var user = await UserManager.GetUserAsync(User); @@ -291,7 +286,6 @@ public async Task ForgetTwoFactorClient() return RedirectToAction(nameof(Index)); } - [Admin(template: nameof(GenerateRecoveryCodes), requireAccessAdminPanelPermission: false)] public async Task GenerateRecoveryCodes() { var user = await UserManager.GetUserAsync(User); @@ -312,7 +306,6 @@ public async Task GenerateRecoveryCodes() } [HttpPost] - [Admin(requireAccessAdminPanelPermission: false)] [ActionName(nameof(GenerateRecoveryCodes))] public async Task GenerateRecoveryCodesPost() { @@ -338,7 +331,6 @@ public async Task GenerateRecoveryCodesPost() return RedirectToAction(nameof(ShowRecoveryCodes)); } - [Admin(template: nameof(ShowRecoveryCodes), requireAccessAdminPanelPermission: false)] public async Task ShowRecoveryCodes() { var user = await UserManager.GetUserAsync(User); @@ -363,7 +355,6 @@ public async Task ShowRecoveryCodes() } [HttpPost] - [Admin(template: nameof(EnableTwoFactorAuthentication), requireAccessAdminPanelPermission: false)] public async Task EnableTwoFactorAuthentication() { var user = await UserManager.GetUserAsync(User); @@ -388,7 +379,6 @@ public async Task EnableTwoFactorAuthentication() return await RedirectToTwoFactorAsync(user); } - [Admin(template: nameof(DisableTwoFactorAuthentication), requireAccessAdminPanelPermission: false)] public async Task DisableTwoFactorAuthentication() { var user = await UserManager.GetUserAsync(User); @@ -408,7 +398,6 @@ public async Task DisableTwoFactorAuthentication() } [HttpPost] - [Admin(requireAccessAdminPanelPermission: false)] [ActionName(nameof(DisableTwoFactorAuthentication))] public async Task DisableTwoFactorAuthenticationPost() { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs b/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs index f519a0b4dda..6e5d638e35f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Filters/TwoFactorAuthenticationAuthorizationFilter.cs @@ -1,48 +1,65 @@ 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.Options; -using OrchardCore.Admin; +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(EmailAuthenticatorController).ControllerName(), + typeof(AuthenticatorAppController).ControllerName(), + typeof(SmsAuthenticatorController).ControllerName(), + ]; + private readonly UserOptions _userOptions; private readonly UserManager _userManager; private readonly ITwoFactorAuthenticationHandlerCoordinator _twoFactorHandlerCoordinator; - private readonly AdminOptions _adminOptions; 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 + "/EnableTwoFactorAuthentication", 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)) { return; } + var routeValues = context.HttpContext.Request.RouteValues; + var areaName = routeValues["area"]?.ToString(); + + if (areaName != null && string.Equals(areaName, UserConstants.Features.Users, StringComparison.OrdinalIgnoreCase)) + { + var controllerName = routeValues["controller"]?.ToString(); + + if (controllerName != null && _allowedControllerNames.Contains(controllerName, StringComparer.OrdinalIgnoreCase)) + { + return; + } + } + if (context.HttpContext.User.HasClaim(claim => claim.Type == UserConstants.TwoFactorAuthenticationClaimType)) { var user = await _userManager.GetUserAsync(context.HttpContext.User); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs index 9890aa630ae..c1a1a02af3e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/UsersThemeSelector.cs @@ -45,9 +45,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/TwoFactorAuthenticationStartup.cs b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs index fb58a9f264a..83819b5e1c7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs @@ -55,6 +55,34 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro pattern: _userOptions.TwoFactorAuthenticationPath, defaults: new { controller = _twoFactorControllerName, 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) } + ); } } @@ -87,6 +115,25 @@ 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)] @@ -105,6 +152,18 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped, TwoFactorMethodLoginEmailDisplayDriver>(); services.AddScoped, EmailAuthenticatorLoginSettingsDisplayDriver>(); } + + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + var _controllerName = typeof(EmailAuthenticatorController).ControllerName(); + + routes.MapAreaControllerRoute( + name: "ConfigureEmailAuthenticator", + areaName: UserConstants.Features.Users, + pattern: "Authenticator/Configure/Email", + defaults: new { controller = _controllerName, action = nameof(EmailAuthenticatorController.Index) } + ); + } } [Feature(UserConstants.Features.SmsAuthenticator)] @@ -124,4 +183,23 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped, TwoFactorMethodLoginSmsDisplayDriver>(); services.AddScoped, SmsAuthenticatorLoginSettingsDisplayDriver>(); } + + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + var _controllerName = typeof(SmsAuthenticatorController).ControllerName(); + + routes.MapAreaControllerRoute( + name: "ConfigureSmsAuthenticator", + areaName: UserConstants.Features.Users, + pattern: "Authenticator/Configure/Sms", + defaults: new { controller = _controllerName, action = nameof(SmsAuthenticatorController.Index) } + ); + + routes.MapAreaControllerRoute( + name: "ConfigureSmsAuthenticatorValidateCode", + areaName: UserConstants.Features.Users, + pattern: "Authenticator/Configure/Sms/ValidateCode", + defaults: new { controller = _controllerName, action = nameof(SmsAuthenticatorController.ValidateCode) } + ); + } } 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"; +} +