diff --git a/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs index 0737ea30d32..57669d887cd 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs @@ -93,7 +93,7 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) } } - [Feature("OrchardCore.Users.Registration")] + [Feature(UserConstants.Features.UserRegistration)] public class RegistrationAdminMenu : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() diff --git a/src/OrchardCore.Modules/OrchardCore.Users/AuditTrail/Registration/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/AuditTrail/Registration/Startup.cs index e518764998c..73b7df56b95 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/AuditTrail/Registration/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/AuditTrail/Registration/Startup.cs @@ -6,7 +6,7 @@ namespace OrchardCore.Users.AuditTrail.Registration { - [RequireFeatures("OrchardCore.Users.AuditTrail", "OrchardCore.Users.Registration")] + [RequireFeatures("OrchardCore.Users.AuditTrail", UserConstants.Features.UserRegistration)] public class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs index 4a24806a212..b872fae1948 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs @@ -15,6 +15,7 @@ using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; +using OrchardCore.Environment.Shell; using OrchardCore.Modules; using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Settings; @@ -39,6 +40,7 @@ public class AccountController : AccountBaseController private readonly ISiteService _siteService; private readonly IEnumerable _accountEvents; private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly IShellFeaturesManager _shellFeaturesManager; private readonly IDisplayManager _loginFormDisplayManager; private readonly IUpdateModelAccessor _updateModelAccessor; private readonly INotifier _notifier; @@ -62,6 +64,7 @@ public AccountController( IClock clock, IDistributedCache distributedCache, IDataProtectionProvider dataProtectionProvider, + IShellFeaturesManager shellFeaturesManager, IDisplayManager loginFormDisplayManager, IUpdateModelAccessor updateModelAccessor, IEnumerable externalLoginHandlers) @@ -76,6 +79,7 @@ public AccountController( _clock = clock; _distributedCache = distributedCache; _dataProtectionProvider = dataProtectionProvider; + _shellFeaturesManager = shellFeaturesManager; _loginFormDisplayManager = loginFormDisplayManager; _updateModelAccessor = updateModelAccessor; _externalLoginHandlers = externalLoginHandlers; @@ -859,6 +863,14 @@ private bool AddUserEnabledError(IUser user) private async Task AddConfirmEmailErrorAsync(IUser user) { + var registrationFeatureIsAvailable = (await _shellFeaturesManager.GetAvailableFeaturesAsync()) + .Any(feature => feature.Id == UserConstants.Features.UserRegistration); + + if (!registrationFeatureIsAvailable) + { + return false; + } + var registrationSettings = (await _siteService.GetSiteSettingsAsync()).As(); if (registrationSettings.UsersMustValidateEmail) { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs index 07e5c0c0fd6..d5272dcfbc8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs @@ -26,6 +26,7 @@ public class AuthenticatorAppController : TwoFactorAuthenticationBaseController { private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&digits={3}&issuer={0}"; + private readonly IdentityOptions _identityOptions; private readonly UrlEncoder _urlEncoder; private readonly ShellSettings _shellSettings; @@ -36,6 +37,7 @@ public AuthenticatorAppController( IHtmlLocalizer htmlLocalizer, IStringLocalizer stringLocalizer, IOptions twoFactorOptions, + IOptions identityOptions, INotifier notifier, IDistributedCache distributedCache, UrlEncoder urlEncoder, @@ -52,6 +54,7 @@ public AuthenticatorAppController( stringLocalizer, twoFactorOptions) { + _identityOptions = identityOptions.Value; _urlEncoder = urlEncoder; _shellSettings = shellSettings; } @@ -88,7 +91,7 @@ public async Task Index(EnableAuthenticatorViewModel model) return View(model); } - var isValid = await UserManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultAuthenticatorProvider, StripToken(model.Code)); + var isValid = await UserManager.VerifyTwoFactorTokenAsync(user, _identityOptions.Tokens.AuthenticatorTokenProvider, StripToken(model.Code)); if (!isValid) { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs index fd8aee15ab5..abc689ea934 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs @@ -14,7 +14,7 @@ namespace OrchardCore.Users.Controllers { - [Feature("OrchardCore.Users.Registration")] + [Feature(UserConstants.Features.UserRegistration)] public class RegistrationController : Controller { private readonly UserManager _userManager; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs index b1a0fa95f09..e2fdf6ce8cd 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs @@ -28,6 +28,7 @@ namespace OrchardCore.Users.Controllers; [Authorize, Feature(UserConstants.Features.SmsAuthenticator)] public class SmsAuthenticatorController : TwoFactorAuthenticationBaseController { + private readonly IdentityOptions _identityOptions; private readonly IUserService _userService; private readonly ISmsService _smsService; private readonly ILiquidTemplateManager _liquidTemplateManager; @@ -41,6 +42,7 @@ public SmsAuthenticatorController( IHtmlLocalizer htmlLocalizer, IStringLocalizer stringLocalizer, IOptions twoFactorOptions, + IOptions identityOptions, INotifier notifier, IDistributedCache distributedCache, IUserService userService, @@ -60,6 +62,7 @@ public SmsAuthenticatorController( stringLocalizer, twoFactorOptions) { + _identityOptions = identityOptions.Value; _userService = userService; _smsService = smsService; _liquidTemplateManager = liquidTemplateManager; @@ -220,7 +223,7 @@ public async Task SendCode() } var settings = (await SiteService.GetSiteSettingsAsync()).As(); - var code = await UserManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider); + var code = await UserManager.GenerateTwoFactorTokenAsync(user, _identityOptions.Tokens.ChangePhoneNumberTokenProvider); var message = new SmsMessage() { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs index 8ebd6d903fe..65b0f06cb26 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs @@ -11,7 +11,7 @@ namespace OrchardCore.Users.Drivers { - [Feature("OrchardCore.Users.Registration")] + [Feature(UserConstants.Features.UserRegistration)] public class RegistrationSettingsDisplayDriver : SectionDisplayDriver { public const string GroupId = "userRegistration"; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs index 331b37e6455..e3660a2a5bc 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs @@ -12,25 +12,41 @@ Id = UserConstants.Features.Users, Name = "Users", Description = "The users module enables authentication UI and user management.", - Dependencies = ["OrchardCore.Roles.Core"], + Dependencies = + [ + "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", Description = "The Change email feature allows users to change their email address.", - Dependencies = [UserConstants.Features.Users], + Dependencies = + [ + UserConstants.Features.Users, + UserConstants.Features.UserEmailConfirmation, + ], Category = "Security" )] [assembly: Feature( - Id = "OrchardCore.Users.Registration", + Id = UserConstants.Features.UserRegistration, Name = "Users Registration", Description = "The registration feature allows external users to sign up to the site and ask to confirm their email.", Dependencies = [ UserConstants.Features.Users, + UserConstants.Features.UserEmailConfirmation, "OrchardCore.Email", ], Category = "Security" @@ -60,7 +76,11 @@ Id = "OrchardCore.Users.Localization", Name = "User Localization", Description = "Provides a way to set the culture per user.", - Dependencies = new[] { "OrchardCore.Users", "OrchardCore.Localization" }, + Dependencies = + [ + "OrchardCore.Users", + "OrchardCore.Localization" + ], Category = "Settings", Priority = "-1" // Added to avoid changing the order in the localization module. )] @@ -100,7 +120,7 @@ [assembly: Feature( Id = UserConstants.Features.TwoFactorAuthentication, Name = "Two-Factor Authentication Services", - Description = "Provices Two-factor core services.", + Description = "Provides Two-factor core services.", Dependencies = [UserConstants.Features.Users], EnabledByDependencyOnly = true, Category = "Security" @@ -126,6 +146,7 @@ [ UserConstants.Features.Users, UserConstants.Features.TwoFactorAuthentication, + UserConstants.Features.UserEmailConfirmation, "OrchardCore.Liquid", "OrchardCore.Email", ], diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/AuthenticatorAppProviderTwoFactorOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/AuthenticatorAppProviderTwoFactorOptionsConfiguration.cs new file mode 100644 index 00000000000..a1e38510333 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/AuthenticatorAppProviderTwoFactorOptionsConfiguration.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class AuthenticatorAppProviderTwoFactorOptionsConfiguration : IConfigureOptions +{ + private readonly IdentityOptions _identityOptions; + + public AuthenticatorAppProviderTwoFactorOptionsConfiguration(IOptions identityOptions) + { + _identityOptions = identityOptions.Value; + } + + public void Configure(TwoFactorOptions options) + { + if (string.IsNullOrEmpty(_identityOptions.Tokens.AuthenticatorTokenProvider) || + options.Providers.Contains(_identityOptions.Tokens.AuthenticatorTokenProvider)) + { + return; + } + + options.Providers.Add(_identityOptions.Tokens.AuthenticatorTokenProvider); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/PhoneProviderTwoFactorOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/PhoneProviderTwoFactorOptionsConfiguration.cs new file mode 100644 index 00000000000..8b4fc2688b5 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/PhoneProviderTwoFactorOptionsConfiguration.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class PhoneProviderTwoFactorOptionsConfiguration : IConfigureOptions +{ + private readonly IdentityOptions _identityOptions; + + public PhoneProviderTwoFactorOptionsConfiguration(IOptions identityOptions) + { + _identityOptions = identityOptions.Value; + } + + public void Configure(TwoFactorOptions options) + { + if (string.IsNullOrEmpty(_identityOptions.Tokens.ChangePhoneNumberTokenProvider) || + options.Providers.Contains(_identityOptions.Tokens.ChangePhoneNumberTokenProvider)) + { + return; + } + + options.Providers.Add(_identityOptions.Tokens.ChangePhoneNumberTokenProvider); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index b878ff8587d..877136cb44f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -75,6 +75,7 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde pattern: _userOptions.LoginPath, defaults: new { controller = _accountControllerName, action = nameof(AccountController.Login) } ); + routes.MapAreaControllerRoute( name: "ChangePassword", areaName: UserConstants.Features.Users, @@ -119,7 +120,7 @@ public override void ConfigureServices(IServiceCollection services) // Add the default token providers used to generate tokens for reset passwords, change email, // and for two-factor authentication token generation. - var identityBuilder = services.AddIdentity(options => + services.AddIdentity(options => { // Specify OrchardCore User requirements. // A user name cannot include an @ symbol, i.e. be an email address @@ -128,17 +129,6 @@ public override void ConfigureServices(IServiceCollection services) options.User.RequireUniqueEmail = true; }); - var phoneNumberProviderType = typeof(PhoneNumberTokenProvider<>).MakeGenericType(identityBuilder.UserType); - identityBuilder.AddTokenProvider(TokenOptions.DefaultPhoneProvider, phoneNumberProviderType); - var emailTokenProviderType = typeof(EmailTokenProvider<>).MakeGenericType(identityBuilder.UserType); - identityBuilder.AddTokenProvider(TokenOptions.DefaultEmailProvider, emailTokenProviderType); - services.Configure(options => - { - options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider; - options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider; - options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider; - options.Tokens.ChangePhoneNumberTokenProvider = TokenOptions.DefaultPhoneProvider; - }); services.AddPhoneFormatValidator(); // Configure the authentication options to use the application cookie scheme as the default sign-out handler. // This is required for security modules like the OpenID module (that uses SignOutAsync()) to work correctly. @@ -153,7 +143,7 @@ public override void ConfigureServices(IServiceCollection services) options.Cookie.Name = "orchauth_" + HttpUtility.UrlEncode(_tenantName); // Don't set the cookie builder 'Path' so that it uses the 'IAuthenticationFeature' value - // set by the pipeline and comming from the request 'PathBase' which already ends with the + // set by the pipeline and coming from the request 'PathBase' which already ends with the // tenant prefix but may also start by a path related e.g to a virtual folder. options.LoginPath = "/" + userOptions.Value.LoginPath; @@ -292,6 +282,17 @@ public override void ConfigureServices(IServiceCollection services) } } + [Feature(UserConstants.Features.UserEmailConfirmation)] + public class EmailConfirmationStartup : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddTransient, EmailConfirmationIdentityOptionsConfigurations>() + .AddTransient() + .AddOptions(); + } + } + [Feature("OrchardCore.Users.ChangeEmail")] public class ChangeEmailStartup : StartupBase { @@ -318,6 +319,10 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { + services.AddTransient, ChangeEmailIdentityOptionsConfigurations>() + .AddTransient() + .AddOptions(); + services.Configure(o => { o.MemberAccessStrategy.Register(); @@ -339,7 +344,7 @@ public override void ConfigureServices(IServiceCollection services) } } - [Feature("OrchardCore.Users.Registration")] + [Feature(UserConstants.Features.UserRegistration)] public class RegistrationStartup : StartupBase { private const string RegisterPath = nameof(RegistrationController.Register); @@ -384,7 +389,7 @@ public override void ConfigureServices(IServiceCollection services) } } - [Feature("OrchardCore.Users.Registration")] + [Feature(UserConstants.Features.UserRegistration)] [RequireFeatures("OrchardCore.Deployment")] public class RegistrationDeploymentStartup : StartupBase { @@ -433,6 +438,10 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { + services.AddTransient, PasswordResetIdentityOptionsConfigurations>() + .AddTransient() + .AddOptions(); + services.Configure(o => { o.MemberAccessStrategy.Register(); @@ -476,6 +485,7 @@ public override void ConfigureServices(IServiceCollection services) } } + [RequireFeatures("OrchardCore.Deployment")] public class UserDeploymentStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs index 6c88617f953..fb58a9f264a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs @@ -76,16 +76,14 @@ public override void ConfigureServices(IServiceCollection services) { var authenticatorProviderType = typeof(AuthenticatorTokenProvider<>).MakeGenericType(typeof(IUser)); services.AddTransient(authenticatorProviderType); + services.Configure(options => { options.Tokens.AuthenticatorTokenProvider = TokenOptions.DefaultAuthenticatorProvider; - options.Tokens.ProviderMap.TryAdd(TokenOptions.DefaultAuthenticatorProvider, new TokenProviderDescriptor(authenticatorProviderType)); + options.Tokens.ProviderMap[TokenOptions.DefaultAuthenticatorProvider] = new TokenProviderDescriptor(authenticatorProviderType); }); - services.Configure(options => - { - options.Providers.Add(TokenOptions.DefaultAuthenticatorProvider); - }); + services.AddTransient, AuthenticatorAppProviderTwoFactorOptionsConfiguration>(); services.AddScoped, AuthenticatorAppLoginSettingsDisplayDriver>(); services.AddScoped, TwoFactorMethodLoginAuthenticationAppDisplayDriver>(); } @@ -96,10 +94,13 @@ public class EmailAuthenticatorStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { - services.Configure(options => - { - options.Providers.Add(TokenOptions.DefaultEmailProvider); - }); + var emailProviderType = typeof(EmailTokenProvider<>).MakeGenericType(typeof(IUser)); + services.AddTransient(emailProviderType) + .Configure(options => options.Providers.Add(TokenOptions.DefaultEmailProvider)) + .Configure(options => + { + options.Tokens.ProviderMap[TokenOptions.DefaultEmailProvider] = new TokenProviderDescriptor(emailProviderType); + }); services.AddScoped, TwoFactorMethodLoginEmailDisplayDriver>(); services.AddScoped, EmailAuthenticatorLoginSettingsDisplayDriver>(); @@ -111,11 +112,15 @@ public class SmsAuthenticatorStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { - services.Configure(options => + var phoneNumberProviderType = typeof(PhoneNumberTokenProvider<>).MakeGenericType(typeof(IUser)); + services.AddTransient(phoneNumberProviderType); + services.Configure(options => { - options.Providers.Add(TokenOptions.DefaultPhoneProvider); + options.Tokens.ChangePhoneNumberTokenProvider = TokenOptions.DefaultPhoneProvider; + options.Tokens.ProviderMap[TokenOptions.DefaultPhoneProvider] = new TokenProviderDescriptor(phoneNumberProviderType); }); + services.AddTransient, PhoneProviderTwoFactorOptionsConfiguration>(); services.AddScoped, TwoFactorMethodLoginSmsDisplayDriver>(); services.AddScoped, SmsAuthenticatorLoginSettingsDisplayDriver>(); } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserConfirmEmail.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserConfirmEmail.cshtml index 8d368ce2a2a..c40ba31fdca 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserConfirmEmail.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserConfirmEmail.cshtml @@ -1,13 +1,19 @@ +@using Microsoft.Extensions.Options +@using OrchardCore.Users.Models +@inject IOptions TokenOptions + @model ConfirmEmailViewModel @{ - Layout = ""; + Layout = string.Empty; }

-@T["Dear {0},", Model.User.UserName] + @T["Dear {0},", Model.User.UserName]

@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["Thank you"]

diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserLostPassword.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserLostPassword.cshtml index 6bbbf9b8a50..503138297e5 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserLostPassword.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/TemplateUserLostPassword.cshtml @@ -1,7 +1,9 @@ +@using Microsoft.Extensions.Options +@using OrchardCore.Users.Models @model LostPasswordViewModel - +@inject IOptions TokenOptions @{ - Layout = ""; + Layout = string.Empty; }

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

@@ -9,3 +11,5 @@

@T["To reset your password please click this link: Reset Password", Model.LostPasswordUrl]

@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)

diff --git a/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs index e304267e87c..4d071924f84 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs @@ -1,17 +1,16 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection.Extensions; using OrchardCore.Data; +using OrchardCore.Json; using OrchardCore.Roles.Services; using OrchardCore.Security; using OrchardCore.Security.Services; using OrchardCore.Users; +using OrchardCore.Users.Core.Json; using OrchardCore.Users.Events; using OrchardCore.Users.Handlers; using OrchardCore.Users.Indexes; using OrchardCore.Users.Services; -using OrchardCore.Users.Core.Json; -using System.Text.Json; -using OrchardCore.Json; namespace Microsoft.Extensions.DependencyInjection { diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/ChangeEmailTokenProviderOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/ChangeEmailTokenProviderOptions.cs new file mode 100644 index 00000000000..abbcb4662e9 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/ChangeEmailTokenProviderOptions.cs @@ -0,0 +1,13 @@ +using System; +using Microsoft.AspNetCore.Identity; + +namespace OrchardCore.Users.Models; + +public sealed class ChangeEmailTokenProviderOptions : DataProtectionTokenProviderOptions +{ + public ChangeEmailTokenProviderOptions() + { + Name = "ChangeEmailDataProtectionTokenProvider"; + TokenLifespan = TimeSpan.FromMinutes(15); + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/EmailConfirmationTokenProviderOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/EmailConfirmationTokenProviderOptions.cs new file mode 100644 index 00000000000..f0ac4a4f35c --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/EmailConfirmationTokenProviderOptions.cs @@ -0,0 +1,13 @@ +using System; +using Microsoft.AspNetCore.Identity; + +namespace OrchardCore.Users.Models; + +public sealed class EmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions +{ + public EmailConfirmationTokenProviderOptions() + { + Name = "EmailConfirmationDataProtectorTokenProvider"; + TokenLifespan = TimeSpan.FromHours(48); + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/PasswordResetTokenProviderOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/PasswordResetTokenProviderOptions.cs new file mode 100644 index 00000000000..dee982c353a --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/PasswordResetTokenProviderOptions.cs @@ -0,0 +1,13 @@ +using System; +using Microsoft.AspNetCore.Identity; + +namespace OrchardCore.Users.Models; + +public sealed class PasswordResetTokenProviderOptions : DataProtectionTokenProviderOptions +{ + public PasswordResetTokenProviderOptions() + { + Name = "PasswordResetDataProtectorTokenProvider"; + TokenLifespan = TimeSpan.FromMinutes(15); + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailIdentityOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailIdentityOptionsConfigurations.cs new file mode 100644 index 00000000000..6fa4a3a88e9 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailIdentityOptionsConfigurations.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class ChangeEmailIdentityOptionsConfigurations : IConfigureOptions +{ + private readonly ChangeEmailTokenProviderOptions _tokenOptions; + + public ChangeEmailIdentityOptionsConfigurations(IOptions tokenOptions) + { + _tokenOptions = tokenOptions.Value; + } + + public void Configure(IdentityOptions options) + { + options.Tokens.ChangeEmailTokenProvider = _tokenOptions.Name; + options.Tokens.ProviderMap[_tokenOptions.Name] = new TokenProviderDescriptor(typeof(ChangeEmailTokenProvider)); + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenProvider.cs new file mode 100644 index 00000000000..e32ce2492e6 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenProvider.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class ChangeEmailTokenProvider : DataProtectorTokenProvider +{ + public ChangeEmailTokenProvider( + IDataProtectionProvider dataProtectionProvider, + IOptions options, + ILogger logger) + : base(dataProtectionProvider, options, logger) + { + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs new file mode 100644 index 00000000000..f1ca192e785 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class EmailConfirmationIdentityOptionsConfigurations : IConfigureOptions +{ + private readonly EmailConfirmationTokenProviderOptions _tokenOptions; + + public EmailConfirmationIdentityOptionsConfigurations(IOptions tokenOptions) + { + _tokenOptions = tokenOptions.Value; + } + + public void Configure(IdentityOptions options) + { + options.Tokens.EmailConfirmationTokenProvider = _tokenOptions.Name; + options.Tokens.ProviderMap[_tokenOptions.Name] = new TokenProviderDescriptor(typeof(EmailConfirmationTokenProvider)); + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenProvider.cs new file mode 100644 index 00000000000..fe9dd7c07f9 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenProvider.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class EmailConfirmationTokenProvider : DataProtectorTokenProvider +{ + public EmailConfirmationTokenProvider( + IDataProtectionProvider dataProtectionProvider, + IOptions options, + ILogger logger) + : base(dataProtectionProvider, options, logger) + { + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetIdentityOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetIdentityOptionsConfigurations.cs new file mode 100644 index 00000000000..f14c7444db1 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetIdentityOptionsConfigurations.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class PasswordResetIdentityOptionsConfigurations : IConfigureOptions +{ + private readonly PasswordResetTokenProviderOptions _tokenOptions; + + public PasswordResetIdentityOptionsConfigurations(IOptions tokenOptions) + { + _tokenOptions = tokenOptions.Value; + } + + public void Configure(IdentityOptions options) + { + options.Tokens.PasswordResetTokenProvider = _tokenOptions.Name; + options.Tokens.ProviderMap[_tokenOptions.Name] = new TokenProviderDescriptor(typeof(PasswordResetTokenProvider)); + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenProvider.cs new file mode 100644 index 00000000000..1557fdd29a8 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenProvider.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class PasswordResetTokenProvider : DataProtectorTokenProvider +{ + public PasswordResetTokenProvider( + IDataProtectionProvider dataProtectionProvider, + IOptions options, + ILogger logger) + : base(dataProtectionProvider, options, logger) + { + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs b/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs index f379967171d..f26bebb564b 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs @@ -15,5 +15,9 @@ public class Features public const string EmailAuthenticator = "OrchardCore.Users.2FA.Email"; public const string SmsAuthenticator = "OrchardCore.Users.2FA.Sms"; + + public const string UserEmailConfirmation = "OrchardCore.Users.EmailConfirmation"; + + public const string UserRegistration = "OrchardCore.Users.Registration"; } } diff --git a/src/docs/reference/modules/Users/README.md b/src/docs/reference/modules/Users/README.md index a567f52748d..23298f9c370 100644 --- a/src/docs/reference/modules/Users/README.md +++ b/src/docs/reference/modules/Users/README.md @@ -12,7 +12,7 @@ The module contains the following features apart from the base feature: - User Time Zone: Provides a way to set the time zone per user. - Custom User Settings: See [its own documentation page](CustomUserSettings/README.md). - [Users Authentication Ticket Store](./TicketStore.md): Stores users authentication tickets on server in memory cache instead of cookies. If distributed cache feature is enabled it will store authentication tickets on distributed cache. -- Two-Factor Authentication Services: Provices Two-factor core services. This feature cannot be manually enabled or disable as it is enabled by dependency on demand. +- Two-Factor Authentication Services: Provides Two-factor core services. This feature cannot be manually enabled or disable as it is enabled by dependency on demand. - Two-Factor Email Method: Allows users to two-factor authenticate using an email. - Two-Factor Authenticator App Method: Allows users to two-factor authenticate using any Authenticator App. - User Localization: Allows ability to configure user culture per user from admin UI. diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md index 2ee46f8e1aa..10165566aa2 100644 --- a/src/docs/releases/1.9.0.md +++ b/src/docs/releases/1.9.0.md @@ -252,3 +252,19 @@ Furthermore, the introduction of the `NotificationOptions` provides configuratio - `TotalUnreadNotifications`: This property determines the maximum number of unread notifications displayed in the navigation bar, with a default setting of 10. - `DisableNotificationHtmlBodySanitizer`: By default, the `HtmlBody` of notifications generated from workflows undergoes a sanitization process. However, this property grants the option to bypass this sanitization process. + +### Users Module + +Enhanced functionality has been implemented, giving developers the ability to control the expiration time of different tokens, such as those for password reset, email confirmation, and email change, which are sent through the email service. Below, you'll find a comprehensive list of configurable options along with their default values: + + | Class Name | Default Expiration Value | + | ---------- | ------------------------ | + | `ChangeEmailTokenProviderOptions` | The token is valid by default for **15 minutes**. | + | `EmailConfirmationTokenProviderOptions` | The token is valid by default for **48 hours**. | + | `PasswordResetTokenProviderOptions` | The token is valid by default for **15 minutes**. | + + You may change the default values of these options by using the `services.Configure<>` method. For instance, to change the `EmailConfirmationTokenProviderOptions` you can add the following code to your project + + ```csharp + services.Configure(options => options.TokenLifespan = TimeSpan.FromDays(7)); + ```