From 20a097a14735a1b45a565ff2b58e2ae8d2c9502b Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 29 Mar 2024 13:27:00 -0700 Subject: [PATCH 01/23] Configure Token provider from the corresponding provider --- .../OrchardCore.Users/Startup.cs | 29 ++++++++++--------- .../TwoFactorAuthenticationStartup.cs | 11 ++++++- .../UsersServiceCollectionExtensions.cs | 12 ++++++++ 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index 393c5bf417f..17f3b162740 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -119,7 +119,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 +128,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 +142,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; @@ -317,6 +306,12 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { + services.TryAddDefaultEmailProvider() + .Configure(options => + { + options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider; + }); + services.Configure(o => { o.MemberAccessStrategy.Register(); @@ -372,6 +367,12 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { + services.TryAddDefaultEmailProvider() + .Configure(options => + { + options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider; + }); + services.Configure(o => { o.MemberAccessStrategy.Register(); @@ -401,7 +402,6 @@ public class ResetPasswordStartup : StartupBase private const string ResetPasswordConfirmationPath = "ResetPasswordConfirmation"; private const string ResetPasswordControllerName = "ResetPassword"; - public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { routes.MapAreaControllerRoute( @@ -474,6 +474,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..bfa4c0b4227 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs @@ -96,7 +96,8 @@ public class EmailAuthenticatorStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { - services.Configure(options => + services.TryAddDefaultEmailProvider() + .Configure(options => { options.Providers.Add(TokenOptions.DefaultEmailProvider); }); @@ -111,6 +112,14 @@ public class SmsAuthenticatorStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { + var phoneNumberProviderType = typeof(PhoneNumberTokenProvider<>).MakeGenericType(typeof(IUser)); + services.AddTransient(phoneNumberProviderType); + services.Configure(options => + { + options.Tokens.ChangePhoneNumberTokenProvider = TokenOptions.DefaultPhoneProvider; + options.Tokens.ProviderMap.TryAdd(TokenOptions.DefaultPhoneProvider, new TokenProviderDescriptor(phoneNumberProviderType)); + }); + services.Configure(options => { options.Providers.Add(TokenOptions.DefaultPhoneProvider); diff --git a/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs index 07ee12a5fef..05e757c9901 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs @@ -65,5 +65,17 @@ public static IServiceCollection AddUsers(this IServiceCollection services) return services; } + + public static IServiceCollection TryAddDefaultEmailProvider(this IServiceCollection services) + { + var emailTokenProviderType = typeof(EmailTokenProvider<>).MakeGenericType(typeof(IUser)); + services.TryAddTransient(emailTokenProviderType); + services.Configure(options => + { + options.Tokens.ProviderMap.TryAdd(TokenOptions.DefaultEmailProvider, new TokenProviderDescriptor(emailTokenProviderType)); + }); + + return services; + } } } From a0e10f8e4c624ea7b3a2c0f82527c3ffca7f0b1b Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 29 Mar 2024 13:30:12 -0700 Subject: [PATCH 02/23] rename --- src/OrchardCore.Modules/OrchardCore.Users/Startup.cs | 4 ++-- .../OrchardCore.Users/TwoFactorAuthenticationStartup.cs | 2 +- .../Extensions/UsersServiceCollectionExtensions.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index 17f3b162740..a3758df0991 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -306,7 +306,7 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { - services.TryAddDefaultEmailProvider() + services.TryAddDefaultEmailTokenProvider() .Configure(options => { options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider; @@ -367,7 +367,7 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { - services.TryAddDefaultEmailProvider() + services.TryAddDefaultEmailTokenProvider() .Configure(options => { options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs index bfa4c0b4227..58ef0905f61 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs @@ -96,7 +96,7 @@ public class EmailAuthenticatorStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { - services.TryAddDefaultEmailProvider() + services.TryAddDefaultEmailTokenProvider() .Configure(options => { options.Providers.Add(TokenOptions.DefaultEmailProvider); diff --git a/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs index 05e757c9901..187ee865b67 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Extensions/UsersServiceCollectionExtensions.cs @@ -66,7 +66,7 @@ public static IServiceCollection AddUsers(this IServiceCollection services) return services; } - public static IServiceCollection TryAddDefaultEmailProvider(this IServiceCollection services) + public static IServiceCollection TryAddDefaultEmailTokenProvider(this IServiceCollection services) { var emailTokenProviderType = typeof(EmailTokenProvider<>).MakeGenericType(typeof(IUser)); services.TryAddTransient(emailTokenProviderType); From 574acbdc90946d86b1b3430d4d2e1b5118fa5f93 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sat, 6 Apr 2024 21:08:33 -0700 Subject: [PATCH 03/23] Update Startup.cs Co-authored-by: Hisham Bin Ateya --- src/OrchardCore.Modules/OrchardCore.Users/Startup.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index a3758df0991..1e7e29a3e07 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -307,10 +307,7 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { services.TryAddDefaultEmailTokenProvider() - .Configure(options => - { - options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider; - }); + .Configure(options => options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider); services.Configure(o => { From 187643813d202ce9ceb8f7241c8386cd8cc0fec3 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sat, 6 Apr 2024 21:08:50 -0700 Subject: [PATCH 04/23] Update Startup.cs Co-authored-by: Hisham Bin Ateya --- src/OrchardCore.Modules/OrchardCore.Users/Startup.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index 1e7e29a3e07..a11efc26e42 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -365,10 +365,7 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { services.TryAddDefaultEmailTokenProvider() - .Configure(options => - { - options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider; - }); + .Configure(options => options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider); services.Configure(o => { From d04b1a6e0a080fd435958898e7479df161f27a4b Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sun, 7 Apr 2024 16:06:52 -0700 Subject: [PATCH 05/23] Add a way to configure the email-confirmation, password-reset, change-email token --- .../EmailAuthenticatorController.cs | 2 +- .../TwoFactorMethodLoginEmailDisplayDriver.cs | 2 +- .../OrchardCore.Users/Startup.cs | 20 +++++++++++++---- .../TwoFactorAuthenticationStartup.cs | 9 ++++---- .../Views/TemplateUserConfirmEmail.cshtml | 10 +++++++-- .../Views/TemplateUserLostPassword.cshtml | 8 +++++-- .../UsersServiceCollectionExtensions.cs | 17 ++------------ .../Models/ChangeEmailTokenProviderOptions.cs | 13 +++++++++++ .../EmailConfirmationTokenProviderOptions.cs | 13 +++++++++++ .../PasswordResetTokenProviderOptions.cs | 13 +++++++++++ .../TwoFactorEmailTokenProviderOptions.cs | 13 +++++++++++ .../Models/TwoFactorOptions.cs | 5 +++++ ...hangeEmailIdentityOptionsConfigurations.cs | 19 ++++++++++++++++ .../ChangeEmailTokenOptionsConfigurations.cs | 20 +++++++++++++++++ .../Services/ChangeEmailTokenProvider.cs | 18 +++++++++++++++ ...nfirmationIdentityOptionsConfigurations.cs | 19 ++++++++++++++++ ...lConfirmationTokenOptionsConfigurations.cs | 20 +++++++++++++++++ .../EmailConfirmationTokenProvider.cs | 18 +++++++++++++++ ...swordResetIdentityOptionsConfigurations.cs | 19 ++++++++++++++++ ...PasswordResetTokenOptionsConfigurations.cs | 20 +++++++++++++++++ .../Services/PasswordResetTokenProvider.cs | 19 ++++++++++++++++ .../Services/TwoFactorEmailTokenProvider.cs | 22 +++++++++++++++++++ src/docs/reference/modules/Users/README.md | 2 +- src/docs/releases/1.9.0.md | 11 +++++++++- 24 files changed, 301 insertions(+), 31 deletions(-) create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/ChangeEmailTokenProviderOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/EmailConfirmationTokenProviderOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/PasswordResetTokenProviderOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailIdentityOptionsConfigurations.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenOptionsConfigurations.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenProvider.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenOptionsConfigurations.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenProvider.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetIdentityOptionsConfigurations.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenOptionsConfigurations.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenProvider.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs index 2a692bdb5d0..c7172bdc284 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs @@ -168,7 +168,7 @@ public async Task SendCode() } var settings = (await SiteService.GetSiteSettingsAsync()).As(); - var code = await UserManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider); + var code = await UserManager.GenerateTwoFactorTokenAsync(user, TwoFactorOptions.TwoFactorEmailProvider); var message = new MailMessage() { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorMethodLoginEmailDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorMethodLoginEmailDisplayDriver.cs index bcc377d1c28..65c128a9d0b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorMethodLoginEmailDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorMethodLoginEmailDisplayDriver.cs @@ -11,6 +11,6 @@ public override IDisplayResult Edit(TwoFactorMethod model) { return View("EmailAuthenticatorValidation", model) .Location("Content") - .OnGroup(TokenOptions.DefaultEmailProvider); + .OnGroup(TwoFactorOptions.TwoFactorEmailProvider); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index a11efc26e42..57f5cbb1760 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -306,8 +306,11 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { - services.TryAddDefaultEmailTokenProvider() - .Configure(options => options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider); + services.AddOptions(); + + services.AddTransient, ChangeEmailTokenOptionsConfigurations>() + .AddTransient, ChangeEmailIdentityOptionsConfigurations>() + .TryAddTransient(); services.Configure(o => { @@ -364,8 +367,11 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { - services.TryAddDefaultEmailTokenProvider() - .Configure(options => options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider); + services.AddOptions(); + + services.AddTransient, EmailConfirmationTokenOptionsConfigurations>() + .AddTransient, EmailConfirmationIdentityOptionsConfigurations>() + .TryAddTransient(); services.Configure(o => { @@ -426,6 +432,12 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { + services.AddOptions(); + + services.AddTransient, PasswordResetIdentityOptionsConfigurations>() + .AddTransient, PasswordResetTokenOptionsConfigurations>() + .TryAddTransient(); + services.Configure(o => { o.MemberAccessStrategy.Register(); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs index 58ef0905f61..d8c81a80062 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.Modules; @@ -96,11 +97,11 @@ public class EmailAuthenticatorStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { - services.TryAddDefaultEmailTokenProvider() - .Configure(options => + services.Configure(options => { - options.Providers.Add(TokenOptions.DefaultEmailProvider); - }); + options.Tokens.ProviderMap.TryAdd(TwoFactorOptions.TwoFactorEmailProvider, new TokenProviderDescriptor(typeof(TwoFactorEmailTokenProvider))); + }).Configure(options => options.Providers.Add(TwoFactorOptions.TwoFactorEmailProvider)) + .TryAddTransient(); services.AddScoped, TwoFactorMethodLoginEmailDisplayDriver>(); services.AddScoped, EmailAuthenticatorLoginSettingsDisplayDriver>(); 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 7caf95aca9c..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 { @@ -65,17 +64,5 @@ public static IServiceCollection AddUsers(this IServiceCollection services) return services; } - - public static IServiceCollection TryAddDefaultEmailTokenProvider(this IServiceCollection services) - { - var emailTokenProviderType = typeof(EmailTokenProvider<>).MakeGenericType(typeof(IUser)); - services.TryAddTransient(emailTokenProviderType); - services.Configure(options => - { - options.Tokens.ProviderMap.TryAdd(TokenOptions.DefaultEmailProvider, new TokenProviderDescriptor(emailTokenProviderType)); - }); - - return services; - } } } 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..85e3fcc033a --- /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(30); + } +} 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/Models/TwoFactorEmailTokenProviderOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs new file mode 100644 index 00000000000..5e98f2c90c5 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs @@ -0,0 +1,13 @@ +using System; +using Microsoft.AspNetCore.Identity; + +namespace OrchardCore.Users.Models; + +public sealed class TwoFactorEmailTokenProviderOptions : DataProtectionTokenProviderOptions +{ + public TwoFactorEmailTokenProviderOptions() + { + Name = "TwoFactorEmailDataProtectorTokenProvider"; + TokenLifespan = TimeSpan.FromMinutes(5); + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorOptions.cs index bdfa10744c0..c0c45131507 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorOptions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorOptions.cs @@ -4,5 +4,10 @@ namespace OrchardCore.Users.Models; public class TwoFactorOptions { + /// + /// Default token provider name used by the two-factor email provider. + /// + public const string TwoFactorEmailProvider = "TwoFactorEmailProvider"; + public IList Providers { get; init; } = []; } 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..325e9fd7c4e --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailIdentityOptionsConfigurations.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; + +namespace OrchardCore.Users.Services; + +public sealed class ChangeEmailIdentityOptionsConfigurations : IConfigureOptions +{ + private readonly TokenOptions _tokenOptions; + + public ChangeEmailIdentityOptionsConfigurations(IOptions tokenOptions) + { + _tokenOptions = tokenOptions.Value; + } + + public void Configure(IdentityOptions options) + { + options.Tokens.ProviderMap.TryAdd(_tokenOptions.ChangeEmailTokenProvider, new TokenProviderDescriptor(typeof(ChangeEmailTokenProvider))); + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenOptionsConfigurations.cs new file mode 100644 index 00000000000..fe8a659092a --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenOptionsConfigurations.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class ChangeEmailTokenOptionsConfigurations : IConfigureOptions +{ + private readonly ChangeEmailTokenProviderOptions _options; + + public ChangeEmailTokenOptionsConfigurations(IOptions options) + { + _options = options.Value; + } + + public void Configure(TokenOptions options) + { + options.ChangeEmailTokenProvider = _options.Name; + } +} 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..296ea27b251 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; + +namespace OrchardCore.Users.Services; + +public sealed class EmailConfirmationIdentityOptionsConfigurations : IConfigureOptions +{ + private readonly TokenOptions _tokenOptions; + + public EmailConfirmationIdentityOptionsConfigurations(IOptions tokenOptions) + { + _tokenOptions = tokenOptions.Value; + } + + public void Configure(IdentityOptions options) + { + options.Tokens.ProviderMap.TryAdd(_tokenOptions.EmailConfirmationTokenProvider, new TokenProviderDescriptor(typeof(EmailConfirmationTokenProvider))); + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenOptionsConfigurations.cs new file mode 100644 index 00000000000..b982b3e6cc7 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenOptionsConfigurations.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class EmailConfirmationTokenOptionsConfigurations : IConfigureOptions +{ + private readonly EmailConfirmationTokenProviderOptions _options; + + public EmailConfirmationTokenOptionsConfigurations(IOptions options) + { + _options = options.Value; + } + + public void Configure(TokenOptions options) + { + options.EmailConfirmationTokenProvider = _options.Name; + } +} 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..c67d2c7c090 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetIdentityOptionsConfigurations.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; + +namespace OrchardCore.Users.Services; + +public sealed class PasswordResetIdentityOptionsConfigurations : IConfigureOptions +{ + private readonly TokenOptions _tokenOptions; + + public PasswordResetIdentityOptionsConfigurations(IOptions tokenOptions) + { + _tokenOptions = tokenOptions.Value; + } + + public void Configure(IdentityOptions options) + { + options.Tokens.ProviderMap[_tokenOptions.PasswordResetTokenProvider] = new TokenProviderDescriptor(typeof(PasswordResetTokenProvider)); + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenOptionsConfigurations.cs new file mode 100644 index 00000000000..e4ff7fea78f --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenOptionsConfigurations.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public sealed class PasswordResetTokenOptionsConfigurations : IConfigureOptions +{ + private readonly PasswordResetTokenProviderOptions _options; + + public PasswordResetTokenOptionsConfigurations(IOptions options) + { + _options = options.Value; + } + + public void Configure(TokenOptions options) + { + options.PasswordResetTokenProvider = _options.Name; + } +} 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/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs new file mode 100644 index 00000000000..10a99820915 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs @@ -0,0 +1,22 @@ +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 TwoFactorEmailTokenProvider : DataProtectorTokenProvider +{ + public TwoFactorEmailTokenProvider( + IDataProtectionProvider dataProtectionProvider, + IOptions options, + ILogger logger) + : base(dataProtectionProvider, options, logger) + { + } + + public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, IUser user) + => Task.FromResult(true); +} 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 4673ad1b2ff..ffb36a3472a 100644 --- a/src/docs/releases/1.9.0.md +++ b/src/docs/releases/1.9.0.md @@ -206,7 +206,7 @@ Added a new `Navbar()` function to Liquid to allow building the `Navbar` shape u {{ navbar }} ``` -### Notifications +### Notifications Module The`INotificationMessage` interface was updated to includes the addition of a `Subject` field, which facilitates the rendering of notification titles. Moreover, the existing `Summary` field has been transitioned to HTML format. This adjustment enables the rendering of HTML notifications in both the navigation bar and the notification center. Consequently, HTML notifications can now be created, affording functionalities such as clickable notifications. @@ -215,3 +215,12 @@ 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 + +New options were added for the email token-providers to give you control on configuring the expiration time on each token sent via email. Here is a list of the available options that you can configure: + + | Class Name | Default Expiration Value | + | ----------- | ----------- | + | `PasswordResetTokenProviderOptions` | The token is valid by default for **15 minutes**. | + | `EmailConfirmationTokenProviderOptions` | The token is valid by default for **48 hours**. | + | `ChangeEmailTokenProviderOptions` | The token is valid by default for **30 minutes**. | From 462def11c191814f4f2e6901c0d18a05b50118f9 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sun, 7 Apr 2024 17:38:54 -0700 Subject: [PATCH 06/23] Use 15 mins for email change --- .../Models/ChangeEmailTokenProviderOptions.cs | 2 +- src/docs/releases/1.9.0.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/ChangeEmailTokenProviderOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/ChangeEmailTokenProviderOptions.cs index 85e3fcc033a..abbcb4662e9 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/ChangeEmailTokenProviderOptions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/ChangeEmailTokenProviderOptions.cs @@ -8,6 +8,6 @@ public sealed class ChangeEmailTokenProviderOptions : DataProtectionTokenProvide public ChangeEmailTokenProviderOptions() { Name = "ChangeEmailDataProtectionTokenProvider"; - TokenLifespan = TimeSpan.FromMinutes(30); + TokenLifespan = TimeSpan.FromMinutes(15); } } diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md index d6f9f5aa6aa..8fece4c8bc8 100644 --- a/src/docs/releases/1.9.0.md +++ b/src/docs/releases/1.9.0.md @@ -231,6 +231,6 @@ New options were added for the email token-providers to give you control on conf | Class Name | Default Expiration Value | | ----------- | ----------- | - | `PasswordResetTokenProviderOptions` | The token is valid by default for **15 minutes**. | + | `ChangeEmailTokenProviderOptions` | The token is valid by default for **15 minutes**. | | `EmailConfirmationTokenProviderOptions` | The token is valid by default for **48 hours**. | - | `ChangeEmailTokenProviderOptions` | The token is valid by default for **30 minutes**. | + | `PasswordResetTokenProviderOptions` | The token is valid by default for **15 minutes**. | From 70b53cfde4deed22dbe2211f249a1eb72a72455d Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 8 Apr 2024 15:40:43 -0700 Subject: [PATCH 07/23] Fix the TwoFactor provider --- .../TwoFactorAuthenticationStartup.cs | 2 + ...hod.TwoFactorEmailProvider.Actions.cshtml} | 0 ...hod.TwoFactorEmailProvider.Content.cshtml} | 0 ...Method.TwoFactorEmailProvider.Icon.cshtml} | 0 .../TwoFactorEmailTokenProviderOptions.cs | 17 +- .../Services/Rfc6238TokenLength.cs | 13 ++ .../Services/TwoFactorEmailTokenProvider.cs | 207 +++++++++++++++++- src/docs/releases/1.9.0.md | 13 +- 8 files changed, 236 insertions(+), 16 deletions(-) rename src/OrchardCore.Modules/OrchardCore.Users/Views/Items/{TwoFactorMethod.Email.Actions.cshtml => TwoFactorMethod.TwoFactorEmailProvider.Actions.cshtml} (100%) rename src/OrchardCore.Modules/OrchardCore.Users/Views/Items/{TwoFactorMethod.Email.Content.cshtml => TwoFactorMethod.TwoFactorEmailProvider.Content.cshtml} (100%) rename src/OrchardCore.Modules/OrchardCore.Users/Views/Items/{TwoFactorMethod.Email.Icon.cshtml => TwoFactorMethod.TwoFactorEmailProvider.Icon.cshtml} (100%) create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238TokenLength.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs index d8c81a80062..15fda9da170 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs @@ -97,6 +97,8 @@ public class EmailAuthenticatorStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { + services.AddOptions(); + services.Configure(options => { options.Tokens.ProviderMap.TryAdd(TwoFactorOptions.TwoFactorEmailProvider, new TokenProviderDescriptor(typeof(TwoFactorEmailTokenProvider))); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Actions.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.TwoFactorEmailProvider.Actions.cshtml similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Actions.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.TwoFactorEmailProvider.Actions.cshtml diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Content.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.TwoFactorEmailProvider.Content.cshtml similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Content.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.TwoFactorEmailProvider.Content.cshtml diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Icon.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.TwoFactorEmailProvider.Icon.cshtml similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Icon.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.TwoFactorEmailProvider.Icon.cshtml diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs index 5e98f2c90c5..c06830922a8 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs @@ -1,13 +1,24 @@ using System; -using Microsoft.AspNetCore.Identity; +using OrchardCore.Users.Services; namespace OrchardCore.Users.Models; -public sealed class TwoFactorEmailTokenProviderOptions : DataProtectionTokenProviderOptions +public sealed class TwoFactorEmailTokenProviderOptions { + // + // Summary: + // Gets or sets the amount of time a generated token remains valid. Defaults to + // 5 Minutes. + // + // Value: + // The amount of time a generated token remains valid. + public TimeSpan TokenLifespan { get; set; } + + public Rfc6238TokenLength TokenLength { get; set; } + public TwoFactorEmailTokenProviderOptions() { - Name = "TwoFactorEmailDataProtectorTokenProvider"; TokenLifespan = TimeSpan.FromMinutes(5); + TokenLength = Rfc6238TokenLength.Eight; } } diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238TokenLength.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238TokenLength.cs new file mode 100644 index 00000000000..7b3b2e108c4 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238TokenLength.cs @@ -0,0 +1,13 @@ +namespace OrchardCore.Users.Services; + +public enum Rfc6238TokenLength +{ + Unspecified, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs index 10a99820915..eb10f3e4574 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs @@ -1,22 +1,209 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Net; +using System.Security.Cryptography; +using System.Text; 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 TwoFactorEmailTokenProvider : DataProtectorTokenProvider +public sealed class TwoFactorEmailTokenProvider : IUserTwoFactorTokenProvider { - public TwoFactorEmailTokenProvider( - IDataProtectionProvider dataProtectionProvider, - IOptions options, - ILogger logger) - : base(dataProtectionProvider, options, logger) + private readonly Rfc6238AuthenticationService _service; + private readonly TwoFactorEmailTokenProviderOptions _options; + + public TwoFactorEmailTokenProvider(IOptions options) + { + _options = options.Value; + _service = new Rfc6238AuthenticationService(options.Value.TokenLifespan, options.Value.TokenLength); + } + + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, IUser user) + => Task.FromResult(manager is not null && user is not null); + + public async Task GenerateAsync(string purpose, UserManager manager, IUser user) + { + ArgumentNullException.ThrowIfNull(user); + var token = await manager.CreateSecurityTokenAsync(user); + var modifier = await GetUserModifierAsync(purpose, manager, user); + + return _service.GenerateCode(token, modifier) + .ToString(GetStringFormat(), CultureInfo.InvariantCulture); + } + + public async Task ValidateAsync(string purpose, string token, UserManager manager, IUser user) + { + ArgumentNullException.ThrowIfNull(user); + + if (!int.TryParse(token, out var code)) + { + return false; + } + + var securityToken = await manager.CreateSecurityTokenAsync(user); + var modifier = await GetUserModifierAsync(purpose, manager, user); + + return securityToken != null && + _service.ValidateCode(securityToken, code, modifier); + } + + private string _format; + + private string GetStringFormat() + { + // Number of 0's is length of the generated pin. + _format ??= _options.TokenLength switch + { + Rfc6238TokenLength.Two => "D2", + Rfc6238TokenLength.Three => "D3", + Rfc6238TokenLength.Four => "D4", + Rfc6238TokenLength.Five => "D5", + Rfc6238TokenLength.Six => "D6", + Rfc6238TokenLength.Seven => "D7", + Rfc6238TokenLength.Eight => "D8", + _ => throw new NotSupportedException("Unsupported token length.") + }; + + return _format; + } + /// + /// Returns a constant, provider and user unique modifier used for entropy in generated tokens from user information. + /// + /// The purpose the token will be generated for. + /// The that can be used to retrieve user properties. + /// The user a token should be generated for. + /// + /// The that represents the asynchronous operation, containing a constant modifier for the specified + /// and . + /// + private static async Task GetUserModifierAsync(string purpose, UserManager manager, IUser user) { + ArgumentNullException.ThrowIfNull(user); + var userId = await manager.GetUserIdAsync(user); + + return $"Totp:{purpose}:{userId}"; } +} + +/// +/// The following code is influenced by +/// +internal sealed class Rfc6238AuthenticationService +{ + private static readonly UTF8Encoding _encoding = new(false, true); - public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, IUser user) - => Task.FromResult(true); + private readonly TimeSpan _timeSpan; + private readonly Rfc6238TokenLength _length; + private int? _modulo; + + public Rfc6238AuthenticationService(TimeSpan timeSpan, Rfc6238TokenLength length) + { + _timeSpan = timeSpan; + _length = length; + } + + private int GetModuloValue() + { + // Number of 0's is length of the generated PIN. + _modulo ??= _length switch + { + Rfc6238TokenLength.Two => 100, + Rfc6238TokenLength.Three => 1000, + Rfc6238TokenLength.Four => 10000, + Rfc6238TokenLength.Five => 100000, + Rfc6238TokenLength.Six => 1000000, + Rfc6238TokenLength.Seven => 10000000, + Rfc6238TokenLength.Eight => 100000000, + _ => throw new NotSupportedException("Unsupported token length.") + }; + + return _modulo.Value; + } + + internal int ComputeTOTP(byte[] key, ulong timestepNumber, byte[] modifierBytes) + { + // See https://tools.ietf.org/html/rfc4226 + // We can add an optional modifier. + Span timestepAsBytes = stackalloc byte[sizeof(long)]; + var res = BitConverter.TryWriteBytes(timestepAsBytes, IPAddress.HostToNetworkOrder((long)timestepNumber)); + Debug.Assert(res); + + var modifierCombinedBytes = timestepAsBytes; + if (modifierBytes is not null) + { + modifierCombinedBytes = ApplyModifier(timestepAsBytes, modifierBytes); + } + + Span hash = stackalloc byte[HMACSHA1.HashSizeInBytes]; + res = HMACSHA1.TryHashData(key, modifierCombinedBytes, hash, out var written); + Debug.Assert(res); + Debug.Assert(written == hash.Length); + + // Generate DT string. + var offset = hash[hash.Length - 1] & 0xf; + Debug.Assert(offset + 4 < hash.Length); + var binaryCode = (hash[offset] & 0x7f) << 24 + | (hash[offset + 1] & 0xff) << 16 + | (hash[offset + 2] & 0xff) << 8 + | (hash[offset + 3] & 0xff); + + return binaryCode % GetModuloValue(); + } + + private static byte[] ApplyModifier(Span input, byte[] modifierBytes) + { + var combined = new byte[checked(input.Length + modifierBytes.Length)]; + input.CopyTo(combined); + Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); + + return combined; + } + + /// + /// More info: https://tools.ietf.org/html/rfc6238#section-4 + /// + private ulong GetCurrentTimeStepNumber() + { + var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch; + + return (ulong)(delta.Ticks / _timeSpan.Ticks); + } + + public int GenerateCode(byte[] securityToken, string modifier = null) + { + ArgumentNullException.ThrowIfNull(securityToken); + + var currentTimeStep = GetCurrentTimeStepNumber(); + + var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; + + return ComputeTOTP(securityToken, currentTimeStep, modifierBytes); + } + + public bool ValidateCode(byte[] securityToken, int code, string modifier = null) + { + ArgumentNullException.ThrowIfNull(securityToken); + + var currentTimeStep = GetCurrentTimeStepNumber(); + + var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; + + // Allow a variance of no greater than 9 minutes in either direction. + for (var i = -2; i <= 2; i++) + { + var computedTOTP = ComputeTOTP(securityToken, (ulong)((long)currentTimeStep + i), modifierBytes); + + if (computedTOTP == code) + { + return true; + } + } + + // No match. + return false; + } } diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md index 8fece4c8bc8..2daaa376610 100644 --- a/src/docs/releases/1.9.0.md +++ b/src/docs/releases/1.9.0.md @@ -227,10 +227,17 @@ Furthermore, the introduction of the `NotificationOptions` provides configuratio ### Users Module -New options were added for the email token-providers to give you control on configuring the expiration time on each token sent via email. Here is a list of the available options that you can configure: - + Enhanced functionality has been introduced, empowering developers to manage the expiration time of various tokens, including password-reset, email-confirmation, change-email, and two-factor authentication, delivered via the email service. Below, you'll find a 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**. | + | `TwoFactorEmailTokenProviderOptions` | The token if valid by default for **5 minutes** and the default length of the token is 8 digits. | + + You many 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)); + ``` From aecad0fc2c3128ff5b871c2a1a2327cf9432ad99 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 8 Apr 2024 15:45:31 -0700 Subject: [PATCH 08/23] update comments --- .../Models/TwoFactorEmailTokenProviderOptions.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs index c06830922a8..e7186c0592a 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs @@ -5,15 +5,15 @@ namespace OrchardCore.Users.Models; public sealed class TwoFactorEmailTokenProviderOptions { - // - // Summary: - // Gets or sets the amount of time a generated token remains valid. Defaults to - // 5 Minutes. - // - // Value: - // The amount of time a generated token remains valid. + /// + /// Gets or sets the amount of time a generated token remains valid. + /// The amount of time a generated token remains valid. Default value is 5 minutes. + /// public TimeSpan TokenLifespan { get; set; } + /// + /// Gets or sets the length of the generated token. Default value is 8 digits long. + /// public Rfc6238TokenLength TokenLength { get; set; } public TwoFactorEmailTokenProviderOptions() From 2b09441247de5035fe048b4956bbd5054a335ade Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 8 Apr 2024 15:53:02 -0700 Subject: [PATCH 09/23] Rename --- .../Models/TwoFactorEmailTokenLength.cs | 13 ++++++++ .../TwoFactorEmailTokenProviderOptions.cs | 7 ++-- .../Services/Rfc6238TokenLength.cs | 13 -------- .../Services/TwoFactorEmailTokenProvider.cs | 32 +++++++++---------- 4 files changed, 32 insertions(+), 33 deletions(-) create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenLength.cs delete mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238TokenLength.cs diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenLength.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenLength.cs new file mode 100644 index 00000000000..0f8100f3bfc --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenLength.cs @@ -0,0 +1,13 @@ +namespace OrchardCore.Users.Models; + +public enum TwoFactorEmailTokenLength +{ + Default, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs index e7186c0592a..8b492a55e2e 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs @@ -1,5 +1,4 @@ using System; -using OrchardCore.Users.Services; namespace OrchardCore.Users.Models; @@ -12,13 +11,13 @@ public sealed class TwoFactorEmailTokenProviderOptions public TimeSpan TokenLifespan { get; set; } /// - /// Gets or sets the length of the generated token. Default value is 8 digits long. + /// Gets or sets the generated token's length. Default value is 8 digits long. /// - public Rfc6238TokenLength TokenLength { get; set; } + public TwoFactorEmailTokenLength TokenLength { get; set; } public TwoFactorEmailTokenProviderOptions() { TokenLifespan = TimeSpan.FromMinutes(5); - TokenLength = Rfc6238TokenLength.Eight; + TokenLength = TwoFactorEmailTokenLength.Default; } } diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238TokenLength.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238TokenLength.cs deleted file mode 100644 index 7b3b2e108c4..00000000000 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238TokenLength.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace OrchardCore.Users.Services; - -public enum Rfc6238TokenLength -{ - Unspecified, - Two, - Three, - Four, - Five, - Six, - Seven, - Eight, -} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs index eb10f3e4574..20f8f070c1d 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs @@ -58,13 +58,13 @@ private string GetStringFormat() // Number of 0's is length of the generated pin. _format ??= _options.TokenLength switch { - Rfc6238TokenLength.Two => "D2", - Rfc6238TokenLength.Three => "D3", - Rfc6238TokenLength.Four => "D4", - Rfc6238TokenLength.Five => "D5", - Rfc6238TokenLength.Six => "D6", - Rfc6238TokenLength.Seven => "D7", - Rfc6238TokenLength.Eight => "D8", + TwoFactorEmailTokenLength.Two => "D2", + TwoFactorEmailTokenLength.Three => "D3", + TwoFactorEmailTokenLength.Four => "D4", + TwoFactorEmailTokenLength.Five => "D5", + TwoFactorEmailTokenLength.Six => "D6", + TwoFactorEmailTokenLength.Seven => "D7", + TwoFactorEmailTokenLength.Eight or TwoFactorEmailTokenLength.Default => "D8", _ => throw new NotSupportedException("Unsupported token length.") }; @@ -97,10 +97,10 @@ internal sealed class Rfc6238AuthenticationService private static readonly UTF8Encoding _encoding = new(false, true); private readonly TimeSpan _timeSpan; - private readonly Rfc6238TokenLength _length; + private readonly TwoFactorEmailTokenLength _length; private int? _modulo; - public Rfc6238AuthenticationService(TimeSpan timeSpan, Rfc6238TokenLength length) + public Rfc6238AuthenticationService(TimeSpan timeSpan, TwoFactorEmailTokenLength length) { _timeSpan = timeSpan; _length = length; @@ -111,13 +111,13 @@ private int GetModuloValue() // Number of 0's is length of the generated PIN. _modulo ??= _length switch { - Rfc6238TokenLength.Two => 100, - Rfc6238TokenLength.Three => 1000, - Rfc6238TokenLength.Four => 10000, - Rfc6238TokenLength.Five => 100000, - Rfc6238TokenLength.Six => 1000000, - Rfc6238TokenLength.Seven => 10000000, - Rfc6238TokenLength.Eight => 100000000, + TwoFactorEmailTokenLength.Two => 100, + TwoFactorEmailTokenLength.Three => 1000, + TwoFactorEmailTokenLength.Four => 10000, + TwoFactorEmailTokenLength.Five => 100000, + TwoFactorEmailTokenLength.Six => 1000000, + TwoFactorEmailTokenLength.Seven => 10000000, + TwoFactorEmailTokenLength.Eight or TwoFactorEmailTokenLength.Default => 100000000, _ => throw new NotSupportedException("Unsupported token length.") }; From fbcb3e8a3f794a5525bddd2708058db25efc6f49 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 8 Apr 2024 20:09:57 -0700 Subject: [PATCH 10/23] Update 1.9.0.md Co-authored-by: Hisham Bin Ateya --- src/docs/releases/1.9.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md index 2daaa376610..29506f144d3 100644 --- a/src/docs/releases/1.9.0.md +++ b/src/docs/releases/1.9.0.md @@ -236,7 +236,7 @@ Furthermore, the introduction of the `NotificationOptions` provides configuratio | `PasswordResetTokenProviderOptions` | The token is valid by default for **15 minutes**. | | `TwoFactorEmailTokenProviderOptions` | The token if valid by default for **5 minutes** and the default length of the token is 8 digits. | - You many 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 + 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)); From 8c7b73cb506c1cff786e44d2b4e65d9ea23d1f6a Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 8 Apr 2024 20:10:21 -0700 Subject: [PATCH 11/23] Update 1.9.0.md Co-authored-by: Hisham Bin Ateya --- src/docs/releases/1.9.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md index 29506f144d3..51bbdff3477 100644 --- a/src/docs/releases/1.9.0.md +++ b/src/docs/releases/1.9.0.md @@ -227,7 +227,7 @@ Furthermore, the introduction of the `NotificationOptions` provides configuratio ### Users Module - Enhanced functionality has been introduced, empowering developers to manage the expiration time of various tokens, including password-reset, email-confirmation, change-email, and two-factor authentication, delivered via the email service. Below, you'll find a list of configurable options along with their default values: +Enhanced functionality has been introduced, empowering developers to manage the expiration time of various tokens, including password-reset, email-confirmation, change-email, and two-factor authentication, delivered via the email service. Below, you'll find a list of configurable options along with their default values: | Class Name | Default Expiration Value | | ---------- | ------------------------ | From 16a4cae55eb266d85817d8fc4b2e85a20c85228e Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 8 Apr 2024 20:11:06 -0700 Subject: [PATCH 12/23] Update TwoFactorEmailTokenProvider.cs Co-authored-by: Hisham Bin Ateya --- .../Services/TwoFactorEmailTokenProvider.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs index 20f8f070c1d..9a955d29a96 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs @@ -190,7 +190,10 @@ public bool ValidateCode(byte[] securityToken, int code, string modifier = null) var currentTimeStep = GetCurrentTimeStepNumber(); - var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; + if (var modifierBytes = modifier is not null) + { + _encoding.GetBytes(modifier); + } // Allow a variance of no greater than 9 minutes in either direction. for (var i = -2; i <= 2; i++) From eadcc90258d3adb1e255c19079187eede78bd048 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 8 Apr 2024 20:14:49 -0700 Subject: [PATCH 13/23] Update TwoFactorEmailTokenProvider.cs Co-authored-by: Hisham Bin Ateya --- .../Services/TwoFactorEmailTokenProvider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs index 9a955d29a96..cca909831d2 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs @@ -15,6 +15,8 @@ public sealed class TwoFactorEmailTokenProvider : IUserTwoFactorTokenProvider options) { From a2782e138921e08c03ec37b79503be19f76093be Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 8 Apr 2024 21:17:30 -0700 Subject: [PATCH 14/23] Update TwoFactorEmailTokenProvider.cs Co-authored-by: Hisham Bin Ateya --- .../Services/TwoFactorEmailTokenProvider.cs | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs index cca909831d2..5ec0b452d5a 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs @@ -53,7 +53,13 @@ public async Task ValidateAsync(string purpose, string token, UserManager< _service.ValidateCode(securityToken, code, modifier); } - private string _format; + private static async Task GetUserModifierAsync(string purpose, UserManager manager, IUser user) + { + ArgumentNullException.ThrowIfNull(user); + var userId = await manager.GetUserIdAsync(user); + + return $"Totp:{purpose}:{userId}"; + } private string GetStringFormat() { @@ -72,23 +78,6 @@ private string GetStringFormat() return _format; } - /// - /// Returns a constant, provider and user unique modifier used for entropy in generated tokens from user information. - /// - /// The purpose the token will be generated for. - /// The that can be used to retrieve user properties. - /// The user a token should be generated for. - /// - /// The that represents the asynchronous operation, containing a constant modifier for the specified - /// and . - /// - private static async Task GetUserModifierAsync(string purpose, UserManager manager, IUser user) - { - ArgumentNullException.ThrowIfNull(user); - var userId = await manager.GetUserIdAsync(user); - - return $"Totp:{purpose}:{userId}"; - } } /// From 119befd8b3110278ed416a5df5ea80ef9d256e9c Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 9 Apr 2024 08:13:54 -0700 Subject: [PATCH 15/23] Fix build --- .../Services/TwoFactorEmailTokenProvider.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs index 5ec0b452d5a..5ee050f7ab1 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs @@ -15,7 +15,7 @@ public sealed class TwoFactorEmailTokenProvider : IUserTwoFactorTokenProvider options) @@ -181,15 +181,24 @@ public bool ValidateCode(byte[] securityToken, int code, string modifier = null) var currentTimeStep = GetCurrentTimeStepNumber(); - if (var modifierBytes = modifier is not null) + var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; + + if (_timeSpan.Minutes <= 3) { - _encoding.GetBytes(modifier); - } + // Allow a variance of no greater than 9 minutes in either direction. + for (var i = -2; i <= 2; i++) + { + var computedTOTP = ComputeTOTP(securityToken, (ulong)((long)currentTimeStep + i), modifierBytes); - // Allow a variance of no greater than 9 minutes in either direction. - for (var i = -2; i <= 2; i++) + if (computedTOTP == code) + { + return true; + } + } + } + else { - var computedTOTP = ComputeTOTP(securityToken, (ulong)((long)currentTimeStep + i), modifierBytes); + var computedTOTP = ComputeTOTP(securityToken, currentTimeStep, modifierBytes); if (computedTOTP == code) { From 9941023538febdba3530fa5c876def18cbd1b1ee Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 9 Apr 2024 08:15:28 -0700 Subject: [PATCH 16/23] Use 3 mins by default for TOTP --- .../Models/TwoFactorEmailTokenProviderOptions.cs | 2 +- src/docs/releases/1.9.0.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs index 8b492a55e2e..2351294c10f 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs @@ -17,7 +17,7 @@ public sealed class TwoFactorEmailTokenProviderOptions public TwoFactorEmailTokenProviderOptions() { - TokenLifespan = TimeSpan.FromMinutes(5); + TokenLifespan = TimeSpan.FromMinutes(3); TokenLength = TwoFactorEmailTokenLength.Default; } } diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md index 51bbdff3477..1a4b4c4e4a4 100644 --- a/src/docs/releases/1.9.0.md +++ b/src/docs/releases/1.9.0.md @@ -234,7 +234,7 @@ Enhanced functionality has been introduced, empowering developers to manage the | `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**. | - | `TwoFactorEmailTokenProviderOptions` | The token if valid by default for **5 minutes** and the default length of the token is 8 digits. | + | `TwoFactorEmailTokenProviderOptions` | The token if valid by default for **3 minutes** and the default length of the token is 8 digits. | 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 From 0c7645497f766ef9027eb4559a8b1d7b36c65cf2 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 9 Apr 2024 08:29:07 -0700 Subject: [PATCH 17/23] Fix build --- .../Services/TwoFactorEmailTokenProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs index 5ee050f7ab1..b8356fff61c 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs @@ -130,9 +130,9 @@ internal int ComputeTOTP(byte[] key, ulong timestepNumber, byte[] modifierBytes) } Span hash = stackalloc byte[HMACSHA1.HashSizeInBytes]; +#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms res = HMACSHA1.TryHashData(key, modifierCombinedBytes, hash, out var written); - Debug.Assert(res); - Debug.Assert(written == hash.Length); +#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms // Generate DT string. var offset = hash[hash.Length - 1] & 0xf; From 6ffca4aff20716e33739125c7d8949cf688bd994 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 9 Apr 2024 08:56:21 -0700 Subject: [PATCH 18/23] Use IClock --- .../Services/TwoFactorEmailTokenProvider.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs index b8356fff61c..16dd522de68 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; +using OrchardCore.Modules; using OrchardCore.Users.Models; namespace OrchardCore.Users.Services; @@ -18,10 +19,12 @@ public sealed class TwoFactorEmailTokenProvider : IUserTwoFactorTokenProvider options) + public TwoFactorEmailTokenProvider( + IOptions options, + IClock clock) { _options = options.Value; - _service = new Rfc6238AuthenticationService(options.Value.TokenLifespan, options.Value.TokenLength); + _service = new Rfc6238AuthenticationService(options.Value.TokenLifespan, options.Value.TokenLength, clock); } public Task CanGenerateTwoFactorTokenAsync(UserManager manager, IUser user) @@ -89,12 +92,18 @@ internal sealed class Rfc6238AuthenticationService private readonly TimeSpan _timeSpan; private readonly TwoFactorEmailTokenLength _length; + private readonly IClock _clock; + private int? _modulo; - public Rfc6238AuthenticationService(TimeSpan timeSpan, TwoFactorEmailTokenLength length) + public Rfc6238AuthenticationService( + TimeSpan timeSpan, + TwoFactorEmailTokenLength length, + IClock clock) { _timeSpan = timeSpan; _length = length; + _clock = clock; } private int GetModuloValue() @@ -159,7 +168,7 @@ private static byte[] ApplyModifier(Span input, byte[] modifierBytes) /// private ulong GetCurrentTimeStepNumber() { - var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch; + var delta = _clock.UtcNow - DateTimeOffset.UnixEpoch; return (ulong)(delta.Ticks / _timeSpan.Ticks); } From 812d56d762cb4047b8006590caadb89c1c98ec83 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 9 Apr 2024 09:06:17 -0700 Subject: [PATCH 19/23] cleanup --- .../Services/Rfc6238AuthenticationService.cs | 146 ++++++++++++++++++ .../Services/TwoFactorEmailTokenProvider.cs | 141 ----------------- 2 files changed, 146 insertions(+), 141 deletions(-) create mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs new file mode 100644 index 00000000000..e5c6167d0c8 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs @@ -0,0 +1,146 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using OrchardCore.Modules; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +/// +/// The following code is influenced by +/// +internal sealed class Rfc6238AuthenticationService +{ + private static readonly UTF8Encoding _encoding = new(false, true); + + private readonly TimeSpan _timeSpan; + private readonly TwoFactorEmailTokenLength _length; + private readonly IClock _clock; + + private int? _modulo; + + public Rfc6238AuthenticationService( + TimeSpan timeSpan, + TwoFactorEmailTokenLength length, + IClock clock) + { + _timeSpan = timeSpan; + _length = length; + _clock = clock; + } + + private int GetModuloValue() + { + // Number of 0's is length of the generated PIN. + _modulo ??= _length switch + { + TwoFactorEmailTokenLength.Two => 100, + TwoFactorEmailTokenLength.Three => 1000, + TwoFactorEmailTokenLength.Four => 10000, + TwoFactorEmailTokenLength.Five => 100000, + TwoFactorEmailTokenLength.Six => 1000000, + TwoFactorEmailTokenLength.Seven => 10000000, + TwoFactorEmailTokenLength.Eight or TwoFactorEmailTokenLength.Default => 100000000, + _ => throw new NotSupportedException("Unsupported token length.") + }; + + return _modulo.Value; + } + + internal int ComputeTOTP(byte[] key, ulong timestepNumber, byte[] modifierBytes) + { + // See https://tools.ietf.org/html/rfc4226 + // We can add an optional modifier. + Span timestepAsBytes = stackalloc byte[sizeof(long)]; + var res = BitConverter.TryWriteBytes(timestepAsBytes, IPAddress.HostToNetworkOrder((long)timestepNumber)); + Debug.Assert(res); + + var modifierCombinedBytes = timestepAsBytes; + if (modifierBytes is not null) + { + modifierCombinedBytes = ApplyModifier(timestepAsBytes, modifierBytes); + } + + Span hash = stackalloc byte[HMACSHA1.HashSizeInBytes]; +#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms + res = HMACSHA1.TryHashData(key, modifierCombinedBytes, hash, out var written); +#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms + + // Generate DT string. + var offset = hash[hash.Length - 1] & 0xf; + Debug.Assert(offset + 4 < hash.Length); + var binaryCode = (hash[offset] & 0x7f) << 24 + | (hash[offset + 1] & 0xff) << 16 + | (hash[offset + 2] & 0xff) << 8 + | (hash[offset + 3] & 0xff); + + return binaryCode % GetModuloValue(); + } + + private static byte[] ApplyModifier(Span input, byte[] modifierBytes) + { + var combined = new byte[checked(input.Length + modifierBytes.Length)]; + input.CopyTo(combined); + Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); + + return combined; + } + + /// + /// More info: https://tools.ietf.org/html/rfc6238#section-4 + /// + private ulong GetCurrentTimeStepNumber() + { + var delta = _clock.UtcNow - DateTimeOffset.UnixEpoch; + + return (ulong)(delta.Ticks / _timeSpan.Ticks); + } + + public int GenerateCode(byte[] securityToken, string modifier = null) + { + ArgumentNullException.ThrowIfNull(securityToken); + + var currentTimeStep = GetCurrentTimeStepNumber(); + + var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; + + return ComputeTOTP(securityToken, currentTimeStep, modifierBytes); + } + + public bool ValidateCode(byte[] securityToken, int code, string modifier = null) + { + ArgumentNullException.ThrowIfNull(securityToken); + + var currentTimeStep = GetCurrentTimeStepNumber(); + + var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; + + if (_timeSpan.Minutes <= 3) + { + // Allow a variance of no greater than 9 minutes in either direction. + for (var i = -2; i <= 2; i++) + { + var computedTOTP = ComputeTOTP(securityToken, (ulong)((long)currentTimeStep + i), modifierBytes); + + if (computedTOTP == code) + { + return true; + } + } + } + else + { + var computedTOTP = ComputeTOTP(securityToken, currentTimeStep, modifierBytes); + + if (computedTOTP == code) + { + return true; + } + } + + // No match. + return false; + } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs index 16dd522de68..6553aa6e881 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs @@ -1,9 +1,5 @@ using System; -using System.Diagnostics; using System.Globalization; -using System.Net; -using System.Security.Cryptography; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; @@ -82,140 +78,3 @@ private string GetStringFormat() return _format; } } - -/// -/// The following code is influenced by -/// -internal sealed class Rfc6238AuthenticationService -{ - private static readonly UTF8Encoding _encoding = new(false, true); - - private readonly TimeSpan _timeSpan; - private readonly TwoFactorEmailTokenLength _length; - private readonly IClock _clock; - - private int? _modulo; - - public Rfc6238AuthenticationService( - TimeSpan timeSpan, - TwoFactorEmailTokenLength length, - IClock clock) - { - _timeSpan = timeSpan; - _length = length; - _clock = clock; - } - - private int GetModuloValue() - { - // Number of 0's is length of the generated PIN. - _modulo ??= _length switch - { - TwoFactorEmailTokenLength.Two => 100, - TwoFactorEmailTokenLength.Three => 1000, - TwoFactorEmailTokenLength.Four => 10000, - TwoFactorEmailTokenLength.Five => 100000, - TwoFactorEmailTokenLength.Six => 1000000, - TwoFactorEmailTokenLength.Seven => 10000000, - TwoFactorEmailTokenLength.Eight or TwoFactorEmailTokenLength.Default => 100000000, - _ => throw new NotSupportedException("Unsupported token length.") - }; - - return _modulo.Value; - } - - internal int ComputeTOTP(byte[] key, ulong timestepNumber, byte[] modifierBytes) - { - // See https://tools.ietf.org/html/rfc4226 - // We can add an optional modifier. - Span timestepAsBytes = stackalloc byte[sizeof(long)]; - var res = BitConverter.TryWriteBytes(timestepAsBytes, IPAddress.HostToNetworkOrder((long)timestepNumber)); - Debug.Assert(res); - - var modifierCombinedBytes = timestepAsBytes; - if (modifierBytes is not null) - { - modifierCombinedBytes = ApplyModifier(timestepAsBytes, modifierBytes); - } - - Span hash = stackalloc byte[HMACSHA1.HashSizeInBytes]; -#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms - res = HMACSHA1.TryHashData(key, modifierCombinedBytes, hash, out var written); -#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms - - // Generate DT string. - var offset = hash[hash.Length - 1] & 0xf; - Debug.Assert(offset + 4 < hash.Length); - var binaryCode = (hash[offset] & 0x7f) << 24 - | (hash[offset + 1] & 0xff) << 16 - | (hash[offset + 2] & 0xff) << 8 - | (hash[offset + 3] & 0xff); - - return binaryCode % GetModuloValue(); - } - - private static byte[] ApplyModifier(Span input, byte[] modifierBytes) - { - var combined = new byte[checked(input.Length + modifierBytes.Length)]; - input.CopyTo(combined); - Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); - - return combined; - } - - /// - /// More info: https://tools.ietf.org/html/rfc6238#section-4 - /// - private ulong GetCurrentTimeStepNumber() - { - var delta = _clock.UtcNow - DateTimeOffset.UnixEpoch; - - return (ulong)(delta.Ticks / _timeSpan.Ticks); - } - - public int GenerateCode(byte[] securityToken, string modifier = null) - { - ArgumentNullException.ThrowIfNull(securityToken); - - var currentTimeStep = GetCurrentTimeStepNumber(); - - var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; - - return ComputeTOTP(securityToken, currentTimeStep, modifierBytes); - } - - public bool ValidateCode(byte[] securityToken, int code, string modifier = null) - { - ArgumentNullException.ThrowIfNull(securityToken); - - var currentTimeStep = GetCurrentTimeStepNumber(); - - var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; - - if (_timeSpan.Minutes <= 3) - { - // Allow a variance of no greater than 9 minutes in either direction. - for (var i = -2; i <= 2; i++) - { - var computedTOTP = ComputeTOTP(securityToken, (ulong)((long)currentTimeStep + i), modifierBytes); - - if (computedTOTP == code) - { - return true; - } - } - } - else - { - var computedTOTP = ComputeTOTP(securityToken, currentTimeStep, modifierBytes); - - if (computedTOTP == code) - { - return true; - } - } - - // No match. - return false; - } -} From f5f20f5b35c6beb49476ff2b78eff521020d0e62 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 9 Apr 2024 16:44:04 -0700 Subject: [PATCH 20/23] Add a test --- .../Services/Rfc6238AuthenticationService.cs | 53 ++++---- .../Services/TwoFactorEmailTokenProvider.cs | 26 +--- .../Rfc6238AuthenticationServiceTests.cs | 122 ++++++++++++++++++ 3 files changed, 156 insertions(+), 45 deletions(-) create mode 100644 test/OrchardCore.Tests/Security/Rfc6238AuthenticationServiceTests.cs diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs index e5c6167d0c8..3e6704a4705 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Diagnostics; +using System.Globalization; using System.Net; using System.Security.Cryptography; using System.Text; @@ -11,7 +12,7 @@ namespace OrchardCore.Users.Services; /// /// The following code is influenced by /// -internal sealed class Rfc6238AuthenticationService +public sealed class Rfc6238AuthenticationService { private static readonly UTF8Encoding _encoding = new(false, true); @@ -20,6 +21,7 @@ internal sealed class Rfc6238AuthenticationService private readonly IClock _clock; private int? _modulo; + private string _format; public Rfc6238AuthenticationService( TimeSpan timeSpan, @@ -49,7 +51,28 @@ private int GetModuloValue() return _modulo.Value; } - internal int ComputeTOTP(byte[] key, ulong timestepNumber, byte[] modifierBytes) + private string GetStringFormat() + { + // Number of 0's is length of the generated pin. + _format ??= _length switch + { + TwoFactorEmailTokenLength.Two => "D2", + TwoFactorEmailTokenLength.Three => "D3", + TwoFactorEmailTokenLength.Four => "D4", + TwoFactorEmailTokenLength.Five => "D5", + TwoFactorEmailTokenLength.Six => "D6", + TwoFactorEmailTokenLength.Seven => "D7", + TwoFactorEmailTokenLength.Eight or TwoFactorEmailTokenLength.Default => "D8", + _ => throw new NotSupportedException("Unsupported token length.") + }; + + return _format; + } + + public string GetString(int code) + => code.ToString(GetStringFormat(), CultureInfo.InvariantCulture); + + public int ComputeTOTP(byte[] key, ulong timestepNumber, byte[] modifierBytes) { // See https://tools.ietf.org/html/rfc4226 // We can add an optional modifier. @@ -63,10 +86,8 @@ internal int ComputeTOTP(byte[] key, ulong timestepNumber, byte[] modifierBytes) modifierCombinedBytes = ApplyModifier(timestepAsBytes, modifierBytes); } - Span hash = stackalloc byte[HMACSHA1.HashSizeInBytes]; -#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms - res = HMACSHA1.TryHashData(key, modifierCombinedBytes, hash, out var written); -#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms + Span hash = stackalloc byte[HMACSHA256.HashSizeInBytes]; + res = HMACSHA256.TryHashData(key, modifierCombinedBytes, hash, out var written); // Generate DT string. var offset = hash[hash.Length - 1] & 0xf; @@ -117,22 +138,10 @@ public bool ValidateCode(byte[] securityToken, int code, string modifier = null) var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; - if (_timeSpan.Minutes <= 3) - { - // Allow a variance of no greater than 9 minutes in either direction. - for (var i = -2; i <= 2; i++) - { - var computedTOTP = ComputeTOTP(securityToken, (ulong)((long)currentTimeStep + i), modifierBytes); - - if (computedTOTP == code) - { - return true; - } - } - } - else + // Check the current, previous, and next time steps. + for (var i = -1; i <= 1; i++) { - var computedTOTP = ComputeTOTP(securityToken, currentTimeStep, modifierBytes); + var computedTOTP = ComputeTOTP(securityToken, (ulong)((long)currentTimeStep + i), modifierBytes); if (computedTOTP == code) { diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs index 6553aa6e881..eae5423b898 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; @@ -13,8 +12,6 @@ public sealed class TwoFactorEmailTokenProvider : IUserTwoFactorTokenProvider options, IClock clock) @@ -32,8 +29,9 @@ public async Task GenerateAsync(string purpose, UserManager manag var token = await manager.CreateSecurityTokenAsync(user); var modifier = await GetUserModifierAsync(purpose, manager, user); - return _service.GenerateCode(token, modifier) - .ToString(GetStringFormat(), CultureInfo.InvariantCulture); + var pin = _service.GenerateCode(token, modifier); + + return _service.GetString(pin); } public async Task ValidateAsync(string purpose, string token, UserManager manager, IUser user) @@ -59,22 +57,4 @@ private static async Task GetUserModifierAsync(string purpose, UserManag return $"Totp:{purpose}:{userId}"; } - - private string GetStringFormat() - { - // Number of 0's is length of the generated pin. - _format ??= _options.TokenLength switch - { - TwoFactorEmailTokenLength.Two => "D2", - TwoFactorEmailTokenLength.Three => "D3", - TwoFactorEmailTokenLength.Four => "D4", - TwoFactorEmailTokenLength.Five => "D5", - TwoFactorEmailTokenLength.Six => "D6", - TwoFactorEmailTokenLength.Seven => "D7", - TwoFactorEmailTokenLength.Eight or TwoFactorEmailTokenLength.Default => "D8", - _ => throw new NotSupportedException("Unsupported token length.") - }; - - return _format; - } } diff --git a/test/OrchardCore.Tests/Security/Rfc6238AuthenticationServiceTests.cs b/test/OrchardCore.Tests/Security/Rfc6238AuthenticationServiceTests.cs new file mode 100644 index 00000000000..07a251425e3 --- /dev/null +++ b/test/OrchardCore.Tests/Security/Rfc6238AuthenticationServiceTests.cs @@ -0,0 +1,122 @@ +using OrchardCore.Modules; +using OrchardCore.Users.Models; +using OrchardCore.Users.Services; + +namespace OrchardCore.Tests.Security; + +public class Rfc6238AuthenticationServiceTests +{ + /* + [Theory] + [InlineData(180, 0, true)] + [InlineData(180, 180, true)] + [InlineData(180, 360, false)] + [InlineData(180, 395, false)] + [InlineData(180, 540, false)] + [InlineData(240, 0, true)] + [InlineData(240, 120, true)] + [InlineData(240, 240, true)] + [InlineData(240, 379, false)] + [InlineData(240, 480, false)] + [InlineData(240, 500, false)] + public void ValidateCode_WhenCalled_ReturnsResult(int timeSpanInSecond, int validateAfterSeconds, bool isValid) + { + var timeSpan = TimeSpan.FromSeconds(timeSpanInSecond); + var startTime = DateTime.UtcNow; + + var creationClock = new Mock(); + creationClock.Setup(x => x.UtcNow) + .Returns(startTime); + + var validationClock = new Mock(); + validationClock.Setup(x => x.UtcNow) + .Returns(startTime.AddSeconds(validateAfterSeconds)); + + var factory = new Rfc6238AuthenticationService(timeSpan, TwoFactorEmailTokenLength.Eight, creationClock.Object); + + var validator = new Rfc6238AuthenticationService(timeSpan, TwoFactorEmailTokenLength.Eight, validationClock.Object); + + var modifier = "Totp:TwoFactor:1"; + + var securityStamp = Encoding.Unicode.GetBytes("security-stamp"); + + var pin = factory.GenerateCode(securityStamp, modifier); + + Assert.Equal(isValid, validator.ValidateCode(securityStamp, pin, modifier)); + } + */ + + [Theory] + [InlineData(TwoFactorEmailTokenLength.Two)] + [InlineData(TwoFactorEmailTokenLength.Three)] + [InlineData(TwoFactorEmailTokenLength.Four)] + [InlineData(TwoFactorEmailTokenLength.Five)] + [InlineData(TwoFactorEmailTokenLength.Six)] + [InlineData(TwoFactorEmailTokenLength.Seven)] + [InlineData(TwoFactorEmailTokenLength.Eight)] + [InlineData(TwoFactorEmailTokenLength.Default)] + public void GetString_WhenCalled_ReturnsCorrectLength(TwoFactorEmailTokenLength length) + { + var timeSpan = TimeSpan.FromMinutes(5); + + var creationClock = new Mock(); + creationClock.Setup(x => x.UtcNow) + .Returns(DateTime.UtcNow); + + var factory = new Rfc6238AuthenticationService(timeSpan, length, creationClock.Object); + + var modifier = "Totp:TwoFactor:1"; + + var securityStamp = Encoding.Unicode.GetBytes("security-stamp"); + + if (length == TwoFactorEmailTokenLength.Eight || length == TwoFactorEmailTokenLength.Default) + { + var pin = factory.GenerateCode(securityStamp, modifier); + + Assert.Equal(8, factory.GetString(pin).Length); + } + else if (length == TwoFactorEmailTokenLength.Seven) + { + var pin = factory.GenerateCode(securityStamp, modifier); + + Assert.Equal(7, factory.GetString(pin).Length); + } + else if (length == TwoFactorEmailTokenLength.Six) + { + var pin = factory.GenerateCode(securityStamp, modifier); + + Assert.Equal(6, factory.GetString(pin).Length); + } + else if (length == TwoFactorEmailTokenLength.Five) + { + var pin = factory.GenerateCode(securityStamp, modifier); + + Assert.Equal(5, factory.GetString(pin).Length); + } + else if (length == TwoFactorEmailTokenLength.Four) + { + var pin = factory.GenerateCode(securityStamp, modifier); + + Assert.Equal(4, factory.GetString(pin).Length); + } + else if (length == TwoFactorEmailTokenLength.Three) + { + var pin = factory.GenerateCode(securityStamp, modifier); + + Assert.Equal(3, factory.GetString(pin).Length); + } + else if (length == TwoFactorEmailTokenLength.Two) + { + var pin = factory.GenerateCode(securityStamp, modifier); + + Assert.Equal(2, factory.GetString(pin).Length); + } + else + { + Assert.Throws(() => + { + factory.GenerateCode(securityStamp, modifier); + }); + } + } +} From cfd9c53b555d25109bcbf3241501d737f82020af Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 9 Apr 2024 16:44:27 -0700 Subject: [PATCH 21/23] cleanup --- .../Services/Rfc6238AuthenticationService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs index 3e6704a4705..28c03eba3e2 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs @@ -53,7 +53,6 @@ private int GetModuloValue() private string GetStringFormat() { - // Number of 0's is length of the generated pin. _format ??= _length switch { TwoFactorEmailTokenLength.Two => "D2", From 45b8c3077c7ff6658d84943a83beaefb39e04f7b Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Wed, 10 Apr 2024 12:31:29 -0700 Subject: [PATCH 22/23] Use the default RFC6238 implementation --- .../Controllers/AuthenticatorAppController.cs | 6 +- .../EmailAuthenticatorController.cs | 6 +- .../Controllers/SmsAuthenticatorController.cs | 5 +- .../TwoFactorMethodLoginEmailDisplayDriver.cs | 2 +- .../OrchardCore.Users/Manifest.cs | 29 +++- ...ppProviderTwoFactorOptionsConfiguration.cs | 26 +++ ...neProviderTwoFactorOptionsConfiguration.cs | 26 +++ .../OrchardCore.Users/Startup.cs | 20 ++- .../TwoFactorAuthenticationStartup.cs | 31 ++-- ...l => TwoFactorMethod.Email.Actions.cshtml} | 0 ...l => TwoFactorMethod.Email.Content.cshtml} | 0 ...html => TwoFactorMethod.Email.Icon.cshtml} | 0 .../Models/TwoFactorEmailTokenLength.cs | 13 -- .../TwoFactorEmailTokenProviderOptions.cs | 23 --- .../Models/TwoFactorOptions.cs | 5 - ...nfirmationIdentityOptionsConfigurations.cs | 2 +- .../Services/Rfc6238AuthenticationService.cs | 154 ------------------ .../Services/TwoFactorEmailTokenProvider.cs | 60 ------- .../OrchardCore.Users.Core/UserConstants.cs | 2 + src/docs/releases/1.9.0.md | 3 +- .../Rfc6238AuthenticationServiceTests.cs | 122 -------------- 21 files changed, 121 insertions(+), 414 deletions(-) create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Services/AuthenticatorAppProviderTwoFactorOptionsConfiguration.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Services/PhoneProviderTwoFactorOptionsConfiguration.cs rename src/OrchardCore.Modules/OrchardCore.Users/Views/Items/{TwoFactorMethod.TwoFactorEmailProvider.Actions.cshtml => TwoFactorMethod.Email.Actions.cshtml} (100%) rename src/OrchardCore.Modules/OrchardCore.Users/Views/Items/{TwoFactorMethod.TwoFactorEmailProvider.Content.cshtml => TwoFactorMethod.Email.Content.cshtml} (100%) rename src/OrchardCore.Modules/OrchardCore.Users/Views/Items/{TwoFactorMethod.TwoFactorEmailProvider.Icon.cshtml => TwoFactorMethod.Email.Icon.cshtml} (100%) delete mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenLength.cs delete mode 100644 src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs delete mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs delete mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs delete mode 100644 test/OrchardCore.Tests/Security/Rfc6238AuthenticationServiceTests.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs index 07e5c0c0fd6..8ad526476f5 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs @@ -25,7 +25,7 @@ namespace OrchardCore.Users.Controllers; public class AuthenticatorAppController : TwoFactorAuthenticationBaseController { private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&digits={3}&issuer={0}"; - + private readonly TokenOptions _tokenOptions; private readonly UrlEncoder _urlEncoder; private readonly ShellSettings _shellSettings; @@ -36,6 +36,7 @@ public AuthenticatorAppController( IHtmlLocalizer htmlLocalizer, IStringLocalizer stringLocalizer, IOptions twoFactorOptions, + IOptions tokenOptions, INotifier notifier, IDistributedCache distributedCache, UrlEncoder urlEncoder, @@ -52,6 +53,7 @@ public AuthenticatorAppController( stringLocalizer, twoFactorOptions) { + _tokenOptions = tokenOptions.Value; _urlEncoder = urlEncoder; _shellSettings = shellSettings; } @@ -88,7 +90,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, _tokenOptions.AuthenticatorTokenProvider, StripToken(model.Code)); if (!isValid) { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs index c7172bdc284..f113c2598b3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs @@ -26,6 +26,7 @@ namespace OrchardCore.Users.Controllers; [Authorize, Feature(UserConstants.Features.EmailAuthenticator)] public class EmailAuthenticatorController : TwoFactorAuthenticationBaseController { + private readonly TokenOptions _tokenOptions; private readonly IUserService _userService; private readonly IEmailService _emailService; private readonly ILiquidTemplateManager _liquidTemplateManager; @@ -40,6 +41,7 @@ public EmailAuthenticatorController( IOptions twoFactorOptions, INotifier notifier, IDistributedCache distributedCache, + IOptions tokenOptions, IUserService userService, IEmailService emailService, ILiquidTemplateManager liquidTemplateManager, @@ -56,6 +58,7 @@ public EmailAuthenticatorController( stringLocalizer, twoFactorOptions) { + _tokenOptions = tokenOptions.Value; _userService = userService; _emailService = emailService; _liquidTemplateManager = liquidTemplateManager; @@ -94,6 +97,7 @@ public async Task RequestCode() return RedirectToTwoFactorIndex(); } + var t = _tokenOptions; var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); var settings = (await SiteService.GetSiteSettingsAsync()).As(); @@ -168,7 +172,7 @@ public async Task SendCode() } var settings = (await SiteService.GetSiteSettingsAsync()).As(); - var code = await UserManager.GenerateTwoFactorTokenAsync(user, TwoFactorOptions.TwoFactorEmailProvider); + var code = await UserManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider); var message = new MailMessage() { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs index b1a0fa95f09..1b71a2ee2e4 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 TokenOptions _tokenOptions; 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 tokenOptions, INotifier notifier, IDistributedCache distributedCache, IUserService userService, @@ -60,6 +62,7 @@ public SmsAuthenticatorController( stringLocalizer, twoFactorOptions) { + _tokenOptions = tokenOptions.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, _tokenOptions.ChangePhoneNumberTokenProvider); var message = new SmsMessage() { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorMethodLoginEmailDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorMethodLoginEmailDisplayDriver.cs index 65c128a9d0b..bcc377d1c28 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorMethodLoginEmailDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/TwoFactorMethodLoginEmailDisplayDriver.cs @@ -11,6 +11,6 @@ public override IDisplayResult Edit(TwoFactorMethod model) { return View("EmailAuthenticatorValidation", model) .Location("Content") - .OnGroup(TwoFactorOptions.TwoFactorEmailProvider); + .OnGroup(TokenOptions.DefaultEmailProvider); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs index 331b37e6455..cc1af69cb21 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs @@ -12,15 +12,30 @@ 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" )] @@ -31,6 +46,7 @@ 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 57f5cbb1760..d76a1d3a20e 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, @@ -280,6 +281,19 @@ public override void ConfigureServices(IServiceCollection services) } } + [Feature(UserConstants.Features.UserEmailConfirmation)] + public class EmailConfirmationStartup : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddOptions(); + + services.AddTransient, EmailConfirmationTokenOptionsConfigurations>() + .AddTransient, EmailConfirmationIdentityOptionsConfigurations>() + .AddTransient(); + } + } + [Feature("OrchardCore.Users.ChangeEmail")] public class ChangeEmailStartup : StartupBase { @@ -367,12 +381,6 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { - services.AddOptions(); - - services.AddTransient, EmailConfirmationTokenOptionsConfigurations>() - .AddTransient, EmailConfirmationIdentityOptionsConfigurations>() - .TryAddTransient(); - services.Configure(o => { o.MemberAccessStrategy.Register(); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs index 15fda9da170..fb58a9f264a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/TwoFactorAuthenticationStartup.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.Modules; @@ -77,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>(); } @@ -97,13 +94,13 @@ public class EmailAuthenticatorStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { - services.AddOptions(); - - services.Configure(options => - { - options.Tokens.ProviderMap.TryAdd(TwoFactorOptions.TwoFactorEmailProvider, new TokenProviderDescriptor(typeof(TwoFactorEmailTokenProvider))); - }).Configure(options => options.Providers.Add(TwoFactorOptions.TwoFactorEmailProvider)) - .TryAddTransient(); + 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>(); @@ -120,14 +117,10 @@ public override void ConfigureServices(IServiceCollection services) services.Configure(options => { options.Tokens.ChangePhoneNumberTokenProvider = TokenOptions.DefaultPhoneProvider; - options.Tokens.ProviderMap.TryAdd(TokenOptions.DefaultPhoneProvider, new TokenProviderDescriptor(phoneNumberProviderType)); - }); - - services.Configure(options => - { - options.Providers.Add(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/Items/TwoFactorMethod.TwoFactorEmailProvider.Actions.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Actions.cshtml similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.TwoFactorEmailProvider.Actions.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Actions.cshtml diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.TwoFactorEmailProvider.Content.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Content.cshtml similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.TwoFactorEmailProvider.Content.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Content.cshtml diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.TwoFactorEmailProvider.Icon.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Icon.cshtml similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.TwoFactorEmailProvider.Icon.cshtml rename to src/OrchardCore.Modules/OrchardCore.Users/Views/Items/TwoFactorMethod.Email.Icon.cshtml diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenLength.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenLength.cs deleted file mode 100644 index 0f8100f3bfc..00000000000 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenLength.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace OrchardCore.Users.Models; - -public enum TwoFactorEmailTokenLength -{ - Default, - Two, - Three, - Four, - Five, - Six, - Seven, - Eight, -} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs deleted file mode 100644 index 2351294c10f..00000000000 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorEmailTokenProviderOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace OrchardCore.Users.Models; - -public sealed class TwoFactorEmailTokenProviderOptions -{ - /// - /// Gets or sets the amount of time a generated token remains valid. - /// The amount of time a generated token remains valid. Default value is 5 minutes. - /// - public TimeSpan TokenLifespan { get; set; } - - /// - /// Gets or sets the generated token's length. Default value is 8 digits long. - /// - public TwoFactorEmailTokenLength TokenLength { get; set; } - - public TwoFactorEmailTokenProviderOptions() - { - TokenLifespan = TimeSpan.FromMinutes(3); - TokenLength = TwoFactorEmailTokenLength.Default; - } -} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorOptions.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorOptions.cs index c0c45131507..bdfa10744c0 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorOptions.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/TwoFactorOptions.cs @@ -4,10 +4,5 @@ namespace OrchardCore.Users.Models; public class TwoFactorOptions { - /// - /// Default token provider name used by the two-factor email provider. - /// - public const string TwoFactorEmailProvider = "TwoFactorEmailProvider"; - public IList Providers { get; init; } = []; } diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs index 296ea27b251..1ecdb6241fe 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs @@ -14,6 +14,6 @@ public EmailConfirmationIdentityOptionsConfigurations(IOptions tok public void Configure(IdentityOptions options) { - options.Tokens.ProviderMap.TryAdd(_tokenOptions.EmailConfirmationTokenProvider, new TokenProviderDescriptor(typeof(EmailConfirmationTokenProvider))); + options.Tokens.ProviderMap[_tokenOptions.EmailConfirmationTokenProvider] = new TokenProviderDescriptor(typeof(EmailConfirmationTokenProvider)); } } diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs deleted file mode 100644 index 28c03eba3e2..00000000000 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.Diagnostics; -using System.Globalization; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using OrchardCore.Modules; -using OrchardCore.Users.Models; - -namespace OrchardCore.Users.Services; - -/// -/// The following code is influenced by -/// -public sealed class Rfc6238AuthenticationService -{ - private static readonly UTF8Encoding _encoding = new(false, true); - - private readonly TimeSpan _timeSpan; - private readonly TwoFactorEmailTokenLength _length; - private readonly IClock _clock; - - private int? _modulo; - private string _format; - - public Rfc6238AuthenticationService( - TimeSpan timeSpan, - TwoFactorEmailTokenLength length, - IClock clock) - { - _timeSpan = timeSpan; - _length = length; - _clock = clock; - } - - private int GetModuloValue() - { - // Number of 0's is length of the generated PIN. - _modulo ??= _length switch - { - TwoFactorEmailTokenLength.Two => 100, - TwoFactorEmailTokenLength.Three => 1000, - TwoFactorEmailTokenLength.Four => 10000, - TwoFactorEmailTokenLength.Five => 100000, - TwoFactorEmailTokenLength.Six => 1000000, - TwoFactorEmailTokenLength.Seven => 10000000, - TwoFactorEmailTokenLength.Eight or TwoFactorEmailTokenLength.Default => 100000000, - _ => throw new NotSupportedException("Unsupported token length.") - }; - - return _modulo.Value; - } - - private string GetStringFormat() - { - _format ??= _length switch - { - TwoFactorEmailTokenLength.Two => "D2", - TwoFactorEmailTokenLength.Three => "D3", - TwoFactorEmailTokenLength.Four => "D4", - TwoFactorEmailTokenLength.Five => "D5", - TwoFactorEmailTokenLength.Six => "D6", - TwoFactorEmailTokenLength.Seven => "D7", - TwoFactorEmailTokenLength.Eight or TwoFactorEmailTokenLength.Default => "D8", - _ => throw new NotSupportedException("Unsupported token length.") - }; - - return _format; - } - - public string GetString(int code) - => code.ToString(GetStringFormat(), CultureInfo.InvariantCulture); - - public int ComputeTOTP(byte[] key, ulong timestepNumber, byte[] modifierBytes) - { - // See https://tools.ietf.org/html/rfc4226 - // We can add an optional modifier. - Span timestepAsBytes = stackalloc byte[sizeof(long)]; - var res = BitConverter.TryWriteBytes(timestepAsBytes, IPAddress.HostToNetworkOrder((long)timestepNumber)); - Debug.Assert(res); - - var modifierCombinedBytes = timestepAsBytes; - if (modifierBytes is not null) - { - modifierCombinedBytes = ApplyModifier(timestepAsBytes, modifierBytes); - } - - Span hash = stackalloc byte[HMACSHA256.HashSizeInBytes]; - res = HMACSHA256.TryHashData(key, modifierCombinedBytes, hash, out var written); - - // Generate DT string. - var offset = hash[hash.Length - 1] & 0xf; - Debug.Assert(offset + 4 < hash.Length); - var binaryCode = (hash[offset] & 0x7f) << 24 - | (hash[offset + 1] & 0xff) << 16 - | (hash[offset + 2] & 0xff) << 8 - | (hash[offset + 3] & 0xff); - - return binaryCode % GetModuloValue(); - } - - private static byte[] ApplyModifier(Span input, byte[] modifierBytes) - { - var combined = new byte[checked(input.Length + modifierBytes.Length)]; - input.CopyTo(combined); - Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); - - return combined; - } - - /// - /// More info: https://tools.ietf.org/html/rfc6238#section-4 - /// - private ulong GetCurrentTimeStepNumber() - { - var delta = _clock.UtcNow - DateTimeOffset.UnixEpoch; - - return (ulong)(delta.Ticks / _timeSpan.Ticks); - } - - public int GenerateCode(byte[] securityToken, string modifier = null) - { - ArgumentNullException.ThrowIfNull(securityToken); - - var currentTimeStep = GetCurrentTimeStepNumber(); - - var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; - - return ComputeTOTP(securityToken, currentTimeStep, modifierBytes); - } - - public bool ValidateCode(byte[] securityToken, int code, string modifier = null) - { - ArgumentNullException.ThrowIfNull(securityToken); - - var currentTimeStep = GetCurrentTimeStepNumber(); - - var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; - - // Check the current, previous, and next time steps. - for (var i = -1; i <= 1; i++) - { - var computedTOTP = ComputeTOTP(securityToken, (ulong)((long)currentTimeStep + i), modifierBytes); - - if (computedTOTP == code) - { - return true; - } - } - - // No match. - return false; - } -} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs deleted file mode 100644 index eae5423b898..00000000000 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenProvider.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using OrchardCore.Modules; -using OrchardCore.Users.Models; - -namespace OrchardCore.Users.Services; - -public sealed class TwoFactorEmailTokenProvider : IUserTwoFactorTokenProvider -{ - private readonly Rfc6238AuthenticationService _service; - private readonly TwoFactorEmailTokenProviderOptions _options; - - public TwoFactorEmailTokenProvider( - IOptions options, - IClock clock) - { - _options = options.Value; - _service = new Rfc6238AuthenticationService(options.Value.TokenLifespan, options.Value.TokenLength, clock); - } - - public Task CanGenerateTwoFactorTokenAsync(UserManager manager, IUser user) - => Task.FromResult(manager is not null && user is not null); - - public async Task GenerateAsync(string purpose, UserManager manager, IUser user) - { - ArgumentNullException.ThrowIfNull(user); - var token = await manager.CreateSecurityTokenAsync(user); - var modifier = await GetUserModifierAsync(purpose, manager, user); - - var pin = _service.GenerateCode(token, modifier); - - return _service.GetString(pin); - } - - public async Task ValidateAsync(string purpose, string token, UserManager manager, IUser user) - { - ArgumentNullException.ThrowIfNull(user); - - if (!int.TryParse(token, out var code)) - { - return false; - } - - var securityToken = await manager.CreateSecurityTokenAsync(user); - var modifier = await GetUserModifierAsync(purpose, manager, user); - - return securityToken != null && - _service.ValidateCode(securityToken, code, modifier); - } - - private static async Task GetUserModifierAsync(string purpose, UserManager manager, IUser user) - { - ArgumentNullException.ThrowIfNull(user); - var userId = await manager.GetUserIdAsync(user); - - return $"Totp:{purpose}:{userId}"; - } -} diff --git a/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs b/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs index f379967171d..d34d398fef8 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs @@ -15,5 +15,7 @@ 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"; } } diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md index 1a4b4c4e4a4..fe861677306 100644 --- a/src/docs/releases/1.9.0.md +++ b/src/docs/releases/1.9.0.md @@ -227,14 +227,13 @@ Furthermore, the introduction of the `NotificationOptions` provides configuratio ### Users Module -Enhanced functionality has been introduced, empowering developers to manage the expiration time of various tokens, including password-reset, email-confirmation, change-email, and two-factor authentication, delivered via the email service. Below, you'll find a list of configurable options along with their default values: +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**. | - | `TwoFactorEmailTokenProviderOptions` | The token if valid by default for **3 minutes** and the default length of the token is 8 digits. | 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 diff --git a/test/OrchardCore.Tests/Security/Rfc6238AuthenticationServiceTests.cs b/test/OrchardCore.Tests/Security/Rfc6238AuthenticationServiceTests.cs deleted file mode 100644 index 07a251425e3..00000000000 --- a/test/OrchardCore.Tests/Security/Rfc6238AuthenticationServiceTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -using OrchardCore.Modules; -using OrchardCore.Users.Models; -using OrchardCore.Users.Services; - -namespace OrchardCore.Tests.Security; - -public class Rfc6238AuthenticationServiceTests -{ - /* - [Theory] - [InlineData(180, 0, true)] - [InlineData(180, 180, true)] - [InlineData(180, 360, false)] - [InlineData(180, 395, false)] - [InlineData(180, 540, false)] - [InlineData(240, 0, true)] - [InlineData(240, 120, true)] - [InlineData(240, 240, true)] - [InlineData(240, 379, false)] - [InlineData(240, 480, false)] - [InlineData(240, 500, false)] - public void ValidateCode_WhenCalled_ReturnsResult(int timeSpanInSecond, int validateAfterSeconds, bool isValid) - { - var timeSpan = TimeSpan.FromSeconds(timeSpanInSecond); - var startTime = DateTime.UtcNow; - - var creationClock = new Mock(); - creationClock.Setup(x => x.UtcNow) - .Returns(startTime); - - var validationClock = new Mock(); - validationClock.Setup(x => x.UtcNow) - .Returns(startTime.AddSeconds(validateAfterSeconds)); - - var factory = new Rfc6238AuthenticationService(timeSpan, TwoFactorEmailTokenLength.Eight, creationClock.Object); - - var validator = new Rfc6238AuthenticationService(timeSpan, TwoFactorEmailTokenLength.Eight, validationClock.Object); - - var modifier = "Totp:TwoFactor:1"; - - var securityStamp = Encoding.Unicode.GetBytes("security-stamp"); - - var pin = factory.GenerateCode(securityStamp, modifier); - - Assert.Equal(isValid, validator.ValidateCode(securityStamp, pin, modifier)); - } - */ - - [Theory] - [InlineData(TwoFactorEmailTokenLength.Two)] - [InlineData(TwoFactorEmailTokenLength.Three)] - [InlineData(TwoFactorEmailTokenLength.Four)] - [InlineData(TwoFactorEmailTokenLength.Five)] - [InlineData(TwoFactorEmailTokenLength.Six)] - [InlineData(TwoFactorEmailTokenLength.Seven)] - [InlineData(TwoFactorEmailTokenLength.Eight)] - [InlineData(TwoFactorEmailTokenLength.Default)] - public void GetString_WhenCalled_ReturnsCorrectLength(TwoFactorEmailTokenLength length) - { - var timeSpan = TimeSpan.FromMinutes(5); - - var creationClock = new Mock(); - creationClock.Setup(x => x.UtcNow) - .Returns(DateTime.UtcNow); - - var factory = new Rfc6238AuthenticationService(timeSpan, length, creationClock.Object); - - var modifier = "Totp:TwoFactor:1"; - - var securityStamp = Encoding.Unicode.GetBytes("security-stamp"); - - if (length == TwoFactorEmailTokenLength.Eight || length == TwoFactorEmailTokenLength.Default) - { - var pin = factory.GenerateCode(securityStamp, modifier); - - Assert.Equal(8, factory.GetString(pin).Length); - } - else if (length == TwoFactorEmailTokenLength.Seven) - { - var pin = factory.GenerateCode(securityStamp, modifier); - - Assert.Equal(7, factory.GetString(pin).Length); - } - else if (length == TwoFactorEmailTokenLength.Six) - { - var pin = factory.GenerateCode(securityStamp, modifier); - - Assert.Equal(6, factory.GetString(pin).Length); - } - else if (length == TwoFactorEmailTokenLength.Five) - { - var pin = factory.GenerateCode(securityStamp, modifier); - - Assert.Equal(5, factory.GetString(pin).Length); - } - else if (length == TwoFactorEmailTokenLength.Four) - { - var pin = factory.GenerateCode(securityStamp, modifier); - - Assert.Equal(4, factory.GetString(pin).Length); - } - else if (length == TwoFactorEmailTokenLength.Three) - { - var pin = factory.GenerateCode(securityStamp, modifier); - - Assert.Equal(3, factory.GetString(pin).Length); - } - else if (length == TwoFactorEmailTokenLength.Two) - { - var pin = factory.GenerateCode(securityStamp, modifier); - - Assert.Equal(2, factory.GetString(pin).Length); - } - else - { - Assert.Throws(() => - { - factory.GenerateCode(securityStamp, modifier); - }); - } - } -} From ffcda7e8c806f0e5d48cb7829c057af737c51eaa Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Wed, 10 Apr 2024 15:28:12 -0700 Subject: [PATCH 23/23] Fix services. The issue is generating numeric tokens --- .../OrchardCore.Users/AdminMenu.cs | 2 +- .../AuditTrail/Registration/Startup.cs | 2 +- .../Controllers/AccountController.cs | 13 ++++++++++ .../Controllers/AuthenticatorAppController.cs | 9 ++++--- .../EmailAuthenticatorController.cs | 4 --- .../Controllers/RegistrationController.cs | 2 +- .../Controllers/SmsAuthenticatorController.cs | 8 +++--- .../RegistrationSettingsDisplayDriver.cs | 2 +- .../OrchardCore.Users/Manifest.cs | 2 +- .../OrchardCore.Users/Startup.cs | 26 +++++++------------ ...hangeEmailIdentityOptionsConfigurations.cs | 8 +++--- .../ChangeEmailTokenOptionsConfigurations.cs | 20 -------------- ...nfirmationIdentityOptionsConfigurations.cs | 8 +++--- ...lConfirmationTokenOptionsConfigurations.cs | 20 -------------- ...swordResetIdentityOptionsConfigurations.cs | 8 +++--- ...PasswordResetTokenOptionsConfigurations.cs | 20 -------------- .../OrchardCore.Users.Core/UserConstants.cs | 2 ++ 17 files changed, 54 insertions(+), 102 deletions(-) delete mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenOptionsConfigurations.cs delete mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenOptionsConfigurations.cs delete mode 100644 src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenOptionsConfigurations.cs 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 68939d50549..f4db01969ad 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using OrchardCore.DisplayManagement.Notify; +using OrchardCore.Environment.Shell; using OrchardCore.Modules; using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Settings; @@ -37,10 +38,12 @@ public class AccountController : AccountBaseController private readonly ISiteService _siteService; private readonly IEnumerable _accountEvents; private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly IShellFeaturesManager _shellFeaturesManager; private readonly INotifier _notifier; private readonly IClock _clock; private readonly IDistributedCache _distributedCache; private readonly IEnumerable _externalLoginHandlers; + protected readonly IHtmlLocalizer H; protected readonly IStringLocalizer S; @@ -57,6 +60,7 @@ public AccountController( IClock clock, IDistributedCache distributedCache, IDataProtectionProvider dataProtectionProvider, + IShellFeaturesManager shellFeaturesManager, IEnumerable externalLoginHandlers) { _signInManager = signInManager; @@ -69,6 +73,7 @@ public AccountController( _clock = clock; _distributedCache = distributedCache; _dataProtectionProvider = dataProtectionProvider; + _shellFeaturesManager = shellFeaturesManager; _externalLoginHandlers = externalLoginHandlers; H = htmlLocalizer; @@ -845,6 +850,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 8ad526476f5..d5272dcfbc8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AuthenticatorAppController.cs @@ -25,7 +25,8 @@ namespace OrchardCore.Users.Controllers; public class AuthenticatorAppController : TwoFactorAuthenticationBaseController { private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&digits={3}&issuer={0}"; - private readonly TokenOptions _tokenOptions; + + private readonly IdentityOptions _identityOptions; private readonly UrlEncoder _urlEncoder; private readonly ShellSettings _shellSettings; @@ -36,7 +37,7 @@ public AuthenticatorAppController( IHtmlLocalizer htmlLocalizer, IStringLocalizer stringLocalizer, IOptions twoFactorOptions, - IOptions tokenOptions, + IOptions identityOptions, INotifier notifier, IDistributedCache distributedCache, UrlEncoder urlEncoder, @@ -53,7 +54,7 @@ public AuthenticatorAppController( stringLocalizer, twoFactorOptions) { - _tokenOptions = tokenOptions.Value; + _identityOptions = identityOptions.Value; _urlEncoder = urlEncoder; _shellSettings = shellSettings; } @@ -90,7 +91,7 @@ public async Task Index(EnableAuthenticatorViewModel model) return View(model); } - var isValid = await UserManager.VerifyTwoFactorTokenAsync(user, _tokenOptions.AuthenticatorTokenProvider, 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/EmailAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs index f113c2598b3..2a692bdb5d0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs @@ -26,7 +26,6 @@ namespace OrchardCore.Users.Controllers; [Authorize, Feature(UserConstants.Features.EmailAuthenticator)] public class EmailAuthenticatorController : TwoFactorAuthenticationBaseController { - private readonly TokenOptions _tokenOptions; private readonly IUserService _userService; private readonly IEmailService _emailService; private readonly ILiquidTemplateManager _liquidTemplateManager; @@ -41,7 +40,6 @@ public EmailAuthenticatorController( IOptions twoFactorOptions, INotifier notifier, IDistributedCache distributedCache, - IOptions tokenOptions, IUserService userService, IEmailService emailService, ILiquidTemplateManager liquidTemplateManager, @@ -58,7 +56,6 @@ public EmailAuthenticatorController( stringLocalizer, twoFactorOptions) { - _tokenOptions = tokenOptions.Value; _userService = userService; _emailService = emailService; _liquidTemplateManager = liquidTemplateManager; @@ -97,7 +94,6 @@ public async Task RequestCode() return RedirectToTwoFactorIndex(); } - var t = _tokenOptions; var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); var settings = (await SiteService.GetSiteSettingsAsync()).As(); 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 1b71a2ee2e4..e2fdf6ce8cd 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs @@ -28,7 +28,7 @@ namespace OrchardCore.Users.Controllers; [Authorize, Feature(UserConstants.Features.SmsAuthenticator)] public class SmsAuthenticatorController : TwoFactorAuthenticationBaseController { - private readonly TokenOptions _tokenOptions; + private readonly IdentityOptions _identityOptions; private readonly IUserService _userService; private readonly ISmsService _smsService; private readonly ILiquidTemplateManager _liquidTemplateManager; @@ -42,7 +42,7 @@ public SmsAuthenticatorController( IHtmlLocalizer htmlLocalizer, IStringLocalizer stringLocalizer, IOptions twoFactorOptions, - IOptions tokenOptions, + IOptions identityOptions, INotifier notifier, IDistributedCache distributedCache, IUserService userService, @@ -62,7 +62,7 @@ public SmsAuthenticatorController( stringLocalizer, twoFactorOptions) { - _tokenOptions = tokenOptions.Value; + _identityOptions = identityOptions.Value; _userService = userService; _smsService = smsService; _liquidTemplateManager = liquidTemplateManager; @@ -223,7 +223,7 @@ public async Task SendCode() } var settings = (await SiteService.GetSiteSettingsAsync()).As(); - var code = await UserManager.GenerateTwoFactorTokenAsync(user, _tokenOptions.ChangePhoneNumberTokenProvider); + 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 cc1af69cb21..e3660a2a5bc 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs @@ -40,7 +40,7 @@ )] [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 = diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index d76a1d3a20e..5ec31ba07d4 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -286,11 +286,9 @@ public class EmailConfirmationStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { - services.AddOptions(); - - services.AddTransient, EmailConfirmationTokenOptionsConfigurations>() - .AddTransient, EmailConfirmationIdentityOptionsConfigurations>() - .AddTransient(); + services.AddTransient, EmailConfirmationIdentityOptionsConfigurations>() + .AddTransient() + .AddOptions(); } } @@ -320,11 +318,9 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { - services.AddOptions(); - - services.AddTransient, ChangeEmailTokenOptionsConfigurations>() - .AddTransient, ChangeEmailIdentityOptionsConfigurations>() - .TryAddTransient(); + services.AddTransient, ChangeEmailIdentityOptionsConfigurations>() + .AddTransient() + .AddOptions(); services.Configure(o => { @@ -347,7 +343,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); @@ -391,7 +387,7 @@ public override void ConfigureServices(IServiceCollection services) } } - [Feature("OrchardCore.Users.Registration")] + [Feature(UserConstants.Features.UserRegistration)] [RequireFeatures("OrchardCore.Deployment")] public class RegistrationDeploymentStartup : StartupBase { @@ -440,11 +436,9 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro public override void ConfigureServices(IServiceCollection services) { - services.AddOptions(); - services.AddTransient, PasswordResetIdentityOptionsConfigurations>() - .AddTransient, PasswordResetTokenOptionsConfigurations>() - .TryAddTransient(); + .AddTransient() + .AddOptions(); services.Configure(o => { diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailIdentityOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailIdentityOptionsConfigurations.cs index 325e9fd7c4e..6fa4a3a88e9 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailIdentityOptionsConfigurations.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailIdentityOptionsConfigurations.cs @@ -1,19 +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 TokenOptions _tokenOptions; + private readonly ChangeEmailTokenProviderOptions _tokenOptions; - public ChangeEmailIdentityOptionsConfigurations(IOptions tokenOptions) + public ChangeEmailIdentityOptionsConfigurations(IOptions tokenOptions) { _tokenOptions = tokenOptions.Value; } public void Configure(IdentityOptions options) { - options.Tokens.ProviderMap.TryAdd(_tokenOptions.ChangeEmailTokenProvider, new TokenProviderDescriptor(typeof(ChangeEmailTokenProvider))); + options.Tokens.ChangeEmailTokenProvider = _tokenOptions.Name; + options.Tokens.ProviderMap[_tokenOptions.Name] = new TokenProviderDescriptor(typeof(ChangeEmailTokenProvider)); } } diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenOptionsConfigurations.cs deleted file mode 100644 index fe8a659092a..00000000000 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenOptionsConfigurations.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using OrchardCore.Users.Models; - -namespace OrchardCore.Users.Services; - -public sealed class ChangeEmailTokenOptionsConfigurations : IConfigureOptions -{ - private readonly ChangeEmailTokenProviderOptions _options; - - public ChangeEmailTokenOptionsConfigurations(IOptions options) - { - _options = options.Value; - } - - public void Configure(TokenOptions options) - { - options.ChangeEmailTokenProvider = _options.Name; - } -} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs index 1ecdb6241fe..f1ca192e785 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationIdentityOptionsConfigurations.cs @@ -1,19 +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 TokenOptions _tokenOptions; + private readonly EmailConfirmationTokenProviderOptions _tokenOptions; - public EmailConfirmationIdentityOptionsConfigurations(IOptions tokenOptions) + public EmailConfirmationIdentityOptionsConfigurations(IOptions tokenOptions) { _tokenOptions = tokenOptions.Value; } public void Configure(IdentityOptions options) { - options.Tokens.ProviderMap[_tokenOptions.EmailConfirmationTokenProvider] = new TokenProviderDescriptor(typeof(EmailConfirmationTokenProvider)); + options.Tokens.EmailConfirmationTokenProvider = _tokenOptions.Name; + options.Tokens.ProviderMap[_tokenOptions.Name] = new TokenProviderDescriptor(typeof(EmailConfirmationTokenProvider)); } } diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenOptionsConfigurations.cs deleted file mode 100644 index b982b3e6cc7..00000000000 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenOptionsConfigurations.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using OrchardCore.Users.Models; - -namespace OrchardCore.Users.Services; - -public sealed class EmailConfirmationTokenOptionsConfigurations : IConfigureOptions -{ - private readonly EmailConfirmationTokenProviderOptions _options; - - public EmailConfirmationTokenOptionsConfigurations(IOptions options) - { - _options = options.Value; - } - - public void Configure(TokenOptions options) - { - options.EmailConfirmationTokenProvider = _options.Name; - } -} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetIdentityOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetIdentityOptionsConfigurations.cs index c67d2c7c090..f14c7444db1 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetIdentityOptionsConfigurations.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetIdentityOptionsConfigurations.cs @@ -1,19 +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 TokenOptions _tokenOptions; + private readonly PasswordResetTokenProviderOptions _tokenOptions; - public PasswordResetIdentityOptionsConfigurations(IOptions tokenOptions) + public PasswordResetIdentityOptionsConfigurations(IOptions tokenOptions) { _tokenOptions = tokenOptions.Value; } public void Configure(IdentityOptions options) { - options.Tokens.ProviderMap[_tokenOptions.PasswordResetTokenProvider] = new TokenProviderDescriptor(typeof(PasswordResetTokenProvider)); + options.Tokens.PasswordResetTokenProvider = _tokenOptions.Name; + options.Tokens.ProviderMap[_tokenOptions.Name] = new TokenProviderDescriptor(typeof(PasswordResetTokenProvider)); } } diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenOptionsConfigurations.cs deleted file mode 100644 index e4ff7fea78f..00000000000 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenOptionsConfigurations.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using OrchardCore.Users.Models; - -namespace OrchardCore.Users.Services; - -public sealed class PasswordResetTokenOptionsConfigurations : IConfigureOptions -{ - private readonly PasswordResetTokenProviderOptions _options; - - public PasswordResetTokenOptionsConfigurations(IOptions options) - { - _options = options.Value; - } - - public void Configure(TokenOptions options) - { - options.PasswordResetTokenProvider = _options.Name; - } -} diff --git a/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs b/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs index d34d398fef8..f26bebb564b 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/UserConstants.cs @@ -17,5 +17,7 @@ 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"; } }