Skip to content

Commit

Permalink
Setup 2FA
Browse files Browse the repository at this point in the history
- do not remember a user's 2fa options
- use gov notify to send the emails
- add notify options to configuration
  • Loading branch information
carlsixsmith-moj committed Jun 9, 2024
1 parent 8d51aa6 commit 599ee30
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 78 deletions.
7 changes: 7 additions & 0 deletions src/Application/Common/Interfaces/ICommunicationsService.cs
Original file line number Diff line number Diff line change
@@ -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);
}
7 changes: 0 additions & 7 deletions src/Application/Common/Interfaces/IMailService.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,29 +1,13 @@
namespace Cfo.Cats.Application.Features.Identity.Notifications.SendFactorCode;

public class SendFactorCodeNotificationHandler : INotificationHandler<SendFactorCodeNotification>
public class SendFactorCodeNotificationHandler(
ILogger<SendFactorCodeNotificationHandler> logger,
ICommunicationsService communicationsService
) : INotificationHandler<SendFactorCodeNotification>
{
private readonly IStringLocalizer<SendFactorCodeNotificationHandler> localizer;
private readonly ILogger<SendFactorCodeNotificationHandler> logger;
private readonly IMailService mailService;

public SendFactorCodeNotificationHandler(
IStringLocalizer<SendFactorCodeNotificationHandler> localizer,
ILogger<SendFactorCodeNotificationHandler> 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);
}
}
10 changes: 7 additions & 3 deletions src/Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -125,7 +127,7 @@ string connectionString
}
}

private static IServiceCollection AddServices(this IServiceCollection services)
private static IServiceCollection AddServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<PicklistService>()
.AddSingleton<IPicklistService>(sp =>
Expand All @@ -143,14 +145,16 @@ private static IServiceCollection AddServices(this IServiceCollection services)
service.Initialize();
return service;
});

services.Configure<NotifyOptions>(configuration.GetSection(NotifyOptions.Notify));

return services
.AddSingleton<ISerializer, SystemTextJsonSerializer>()
.AddScoped<ICurrentUserService, CurrentUserService>()
.AddScoped<ITenantProvider, TenantProvider>()
.AddScoped<IValidationService, ValidationService>()
.AddScoped<IDateTime, DateTimeService>()
.AddScoped<IMailService, MailService>()
.AddScoped<ICommunicationsService, CommunicationsService>()
.AddScoped<IExcelService, ExcelService>()
.AddScoped<IUploadService, UploadService>();
}
Expand Down
2 changes: 2 additions & 0 deletions src/Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<ItemGroup>

<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />

<PackageReference Include="GovukNotify" Version="7.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
Expand Down
45 changes: 45 additions & 0 deletions src/Infrastructure/Services/CommunicationsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Notify.Client;

namespace Cfo.Cats.Infrastructure.Services;

public class CommunicationsService(IOptions<NotifyOptions> options, ILogger<CommunicationsService> 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<string, dynamic>() {
{
"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; }
}
14 changes: 0 additions & 14 deletions src/Infrastructure/Services/MailService.cs

This file was deleted.

19 changes: 5 additions & 14 deletions src/Server.UI/Pages/Identity/Authentication/Login.razor
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,6 @@
</div>
</div>
<div Class="d-flex justify-space-between align-center mb-1">
<label class="form-label">
<InputCheckbox @bind-Value="Input.RememberMe" class="form-check-input" />
@L["Remember me"]
</label>
<MudLink Href="@Forgot.PageUrl">@L["Forgot password?"]</MudLink>
</div>

Expand Down Expand Up @@ -123,8 +119,7 @@
private InputModel Input { get; set; } = new()
{
UserName = "",
Password = "",
RememberMe = true
Password = ""
};

protected override async Task OnInitializedAsync()
Expand All @@ -141,22 +136,19 @@
{
// 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.");
RedirectManager.RedirectTo(ReturnUrl);
}
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)
{
Expand All @@ -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;
}
}
19 changes: 1 addition & 18 deletions src/Server.UI/Pages/Identity/Authentication/LoginWith2fa.razor
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,6 @@
</div>
</div>
</div>

<div class="checkbox mb-3">
<label for="remember-machine" class="form-label">
<InputCheckbox @bind-Value="Input.RememberMachine" />
Remember this machine
</label>
</div>
<div>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Expand All @@ -58,13 +51,6 @@
</div>
</EditForm>
</div>

@*
<MudText Typo="Typo.body1">
@L["Don't have access to your authenticator device? You can"]
</MudText>
<a class="mud-button-root mud-button mud-button-text mud-button-text-default mud-button-text-size-medium mud-ripple" href="@($"{LoginWithRecoveryCode.PageUrl}?ReturnUrl={ReturnUrl}")">log in with a recovery code</a>.
*@
</div>

@code {
Expand Down Expand Up @@ -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)
{
Expand All @@ -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; }
}
}
5 changes: 5 additions & 0 deletions src/Server.UI/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,10 @@
"RequireUpperCase": false,
"RequireLowerCase": false,
"DefaultLockoutTimeSpan": 30
},
"Notify": {
"ApiKey": "",
"SmsTemplate": "",
"EmailTemplate": ""
}
}

0 comments on commit 599ee30

Please sign in to comment.