From 599ee30b4167c77811a92a1d1bd7b9b55d88a052 Mon Sep 17 00:00:00 2001 From: Carl Sixsmith Date: Tue, 4 Jun 2024 12:14:49 +0100 Subject: [PATCH] Setup 2FA - do not remember a user's 2fa options - use gov notify to send the emails - add notify options to configuration --- .../Interfaces/ICommunicationsService.cs | 7 +++ .../Common/Interfaces/IMailService.cs | 7 --- .../SendFactorCodeNotificationHandler.cs | 28 +++--------- src/Infrastructure/DependencyInjection.cs | 10 +++-- src/Infrastructure/Infrastructure.csproj | 2 + .../Services/CommunicationsService.cs | 45 +++++++++++++++++++ src/Infrastructure/Services/MailService.cs | 14 ------ .../Pages/Identity/Authentication/Login.razor | 19 +++----- .../Authentication/LoginWith2fa.razor | 19 +------- src/Server.UI/appsettings.json | 5 +++ 10 files changed, 78 insertions(+), 78 deletions(-) create mode 100644 src/Application/Common/Interfaces/ICommunicationsService.cs delete mode 100644 src/Application/Common/Interfaces/IMailService.cs create mode 100644 src/Infrastructure/Services/CommunicationsService.cs delete mode 100644 src/Infrastructure/Services/MailService.cs diff --git a/src/Application/Common/Interfaces/ICommunicationsService.cs b/src/Application/Common/Interfaces/ICommunicationsService.cs new file mode 100644 index 00000000..3bac69a1 --- /dev/null +++ b/src/Application/Common/Interfaces/ICommunicationsService.cs @@ -0,0 +1,7 @@ +namespace Cfo.Cats.Application.Common.Interfaces; + +public interface ICommunicationsService +{ + Task SendSmsCodeAsync(string mobileNumber, string code); + Task SendEmailCodeAsync(string email, string code); +} diff --git a/src/Application/Common/Interfaces/IMailService.cs b/src/Application/Common/Interfaces/IMailService.cs deleted file mode 100644 index 446a31fc..00000000 --- a/src/Application/Common/Interfaces/IMailService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Cfo.Cats.Application.Common.Interfaces; - -public interface IMailService -{ - Task SendAsync(string to, string subject, string body); - Task SendAsync(string to, string subject, string template, object model); -} diff --git a/src/Application/Features/Identity/Notifications/SendFactorCode/SendFactorCodeNotificationHandler.cs b/src/Application/Features/Identity/Notifications/SendFactorCode/SendFactorCodeNotificationHandler.cs index 89d4afe5..bc08679e 100644 --- a/src/Application/Features/Identity/Notifications/SendFactorCode/SendFactorCodeNotificationHandler.cs +++ b/src/Application/Features/Identity/Notifications/SendFactorCode/SendFactorCodeNotificationHandler.cs @@ -1,29 +1,13 @@ namespace Cfo.Cats.Application.Features.Identity.Notifications.SendFactorCode; -public class SendFactorCodeNotificationHandler : INotificationHandler +public class SendFactorCodeNotificationHandler( + ILogger logger, + ICommunicationsService communicationsService +) : INotificationHandler { - private readonly IStringLocalizer localizer; - private readonly ILogger logger; - private readonly IMailService mailService; - - public SendFactorCodeNotificationHandler( - IStringLocalizer localizer, - ILogger logger, - IMailService mailService - ) - { - this.localizer = localizer; - this.logger = logger; - this.mailService = mailService; - } - - public async Task Handle( - SendFactorCodeNotification notification, - CancellationToken cancellationToken - ) + public async Task Handle(SendFactorCodeNotification notification, CancellationToken cancellationToken) { - var subject = localizer["Your Verification Code"]; - await mailService.SendAsync(notification.Email, subject, notification.AuthenticatorCode); + await communicationsService.SendEmailCodeAsync(notification.Email, notification.AuthenticatorCode); logger.LogInformation("Verification Code email sent to {Email})", notification.Email); } } \ No newline at end of file diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index 2e923c1a..8a4b9e56 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -31,7 +31,9 @@ public static IServiceCollection AddInfrastructure( IConfiguration configuration ) { - services.AddSettings(configuration).AddDatabase(configuration).AddServices(); + services.AddSettings(configuration) + .AddDatabase(configuration) + .AddServices(configuration); services.AddAuthenticationService(configuration).AddFusionCacheService(); @@ -125,7 +127,7 @@ string connectionString } } - private static IServiceCollection AddServices(this IServiceCollection services) + private static IServiceCollection AddServices(this IServiceCollection services, IConfiguration configuration) { services.AddSingleton() .AddSingleton(sp => @@ -143,6 +145,8 @@ private static IServiceCollection AddServices(this IServiceCollection services) service.Initialize(); return service; }); + + services.Configure(configuration.GetSection(NotifyOptions.Notify)); return services .AddSingleton() @@ -150,7 +154,7 @@ private static IServiceCollection AddServices(this IServiceCollection services) .AddScoped() .AddScoped() .AddScoped() - .AddScoped() + .AddScoped() .AddScoped() .AddScoped(); } diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index f9e42e5b..347f2f5e 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -12,6 +12,8 @@ + + diff --git a/src/Infrastructure/Services/CommunicationsService.cs b/src/Infrastructure/Services/CommunicationsService.cs new file mode 100644 index 00000000..1f1619d8 --- /dev/null +++ b/src/Infrastructure/Services/CommunicationsService.cs @@ -0,0 +1,45 @@ +using Notify.Client; + +namespace Cfo.Cats.Infrastructure.Services; + +public class CommunicationsService(IOptions options, ILogger logger) : ICommunicationsService +{ + public Task SendSmsCodeAsync(string mobileNumber, string code) + { + throw new NotImplementedException(); + } + public async Task SendEmailCodeAsync(string email, string code) + { + try + { + var client = Client(); + var response = await client.SendEmailAsync(emailAddress: email, + templateId: options.Value.EmailTemplate, + personalisation: new Dictionary() { + { + "subject", + "Your two factor authentication code." + }, + { + "body", $"Your two factor authentication code is {code}" + } + }); + } + catch (Exception e) + { + logger.LogError("Failed to send Email code. {e}", e); + } + } + + private NotificationClient Client() => new(options.Value.ApiKey); +} + +public class NotifyOptions +{ + + public const string Notify = "Notify"; + public string ApiKey { get; set; } + public string SmsTemplate { get; set; } + + public string EmailTemplate { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/Services/MailService.cs b/src/Infrastructure/Services/MailService.cs deleted file mode 100644 index 6a78f026..00000000 --- a/src/Infrastructure/Services/MailService.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Cfo.Cats.Infrastructure.Services; - -public class MailService : IMailService -{ - public Task SendAsync(string to, string subject, string body) - { - throw new NotImplementedException(); - } - - public Task SendAsync(string to, string subject, string template, object model) - { - throw new NotImplementedException(); - } -} diff --git a/src/Server.UI/Pages/Identity/Authentication/Login.razor b/src/Server.UI/Pages/Identity/Authentication/Login.razor index 45b53b93..347a6973 100644 --- a/src/Server.UI/Pages/Identity/Authentication/Login.razor +++ b/src/Server.UI/Pages/Identity/Authentication/Login.razor @@ -88,10 +88,6 @@
- @L["Forgot password?"]
@@ -123,8 +119,7 @@ private InputModel Input { get; set; } = new() { UserName = "", - Password = "", - RememberMe = true + Password = "" }; protected override async Task OnInitializedAsync() @@ -141,7 +136,7 @@ { // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true - var result = await SignInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false); + var result = await SignInManager.PasswordSignInAsync(Input.UserName, Input.Password, false, lockoutOnFailure: true); if (result.Succeeded) { Logger.LogInformation($"{Input.UserName} logged in."); @@ -149,14 +144,11 @@ } else if (result.RequiresTwoFactor) { - //var user = await UserManager.FindByNameAsync(Input.UserName); var user = await SignInManager.GetTwoFactorAuthenticationUserAsync(); - var token = await UserManager.GenerateTwoFactorTokenAsync(user, "Email"); - await Sender.Publish(new SendFactorCodeNotification(user.Email, user.UserName, token)); - - RedirectManager.RedirectTo(LoginWith2fa.PageUrl, new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe }); - + var token = await UserManager.GenerateTwoFactorTokenAsync(user!, "Email"); + await Sender.Publish(new SendFactorCodeNotification(user.Email!, user.UserName!, token)); + RedirectManager.RedirectTo(LoginWith2fa.PageUrl, new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = false }); } else if (result.IsLockedOut) { @@ -177,6 +169,5 @@ [Required(ErrorMessage = "Password cannot be empty")] [StringLength(30, ErrorMessage = "Password must be at least 6 characters long.", MinimumLength = 6)] public string Password { get; set; } = ""; - public bool RememberMe { get; set; } = true; } } \ No newline at end of file diff --git a/src/Server.UI/Pages/Identity/Authentication/LoginWith2fa.razor b/src/Server.UI/Pages/Identity/Authentication/LoginWith2fa.razor index 3735a41e..7b1be66e 100644 --- a/src/Server.UI/Pages/Identity/Authentication/LoginWith2fa.razor +++ b/src/Server.UI/Pages/Identity/Authentication/LoginWith2fa.razor @@ -40,13 +40,6 @@ - -
- -
- - @* - - @L["Don't have access to your authenticator device? You can"] - - log in with a recovery code. - *@ @code { @@ -100,7 +86,7 @@ message = L["Error: Invalid authenticator code."]; return; } - var result = await SignInManager.TwoFactorSignInAsync("Email", authenticatorCode, true, true); + var result = await SignInManager.TwoFactorSignInAsync("Email", authenticatorCode, false, false); var userId = await UserManager.GetUserIdAsync(user); if (result.Succeeded) { @@ -126,8 +112,5 @@ [DataType(DataType.Text)] [Display(Name = "Authenticator code")] public string? TwoFactorCode { get; set; } - - [Display(Name = "Remember this machine")] - public bool RememberMachine { get; set; } } } diff --git a/src/Server.UI/appsettings.json b/src/Server.UI/appsettings.json index 2029dbbb..bacfcc45 100644 --- a/src/Server.UI/appsettings.json +++ b/src/Server.UI/appsettings.json @@ -44,5 +44,10 @@ "RequireUpperCase": false, "RequireLowerCase": false, "DefaultLockoutTimeSpan": 30 + }, + "Notify": { + "ApiKey": "", + "SmsTemplate": "", + "EmailTemplate": "" } } \ No newline at end of file