-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Changes as a result of password and lockout review.
- enable sms 2FA - remember previous passwords and prevent re-use - lockout account after 5 failed attempts
- Loading branch information
1 parent
5ddfbcc
commit cbf926f
Showing
24 changed files
with
2,705 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -160,6 +160,11 @@ BEGIN TRY | |
INSERT INTO [Identity].[User] (Id, DisplayName, ProviderId, TenantId, TenantName, ProfilePictureDataUrl, IsActive, IsLive, MemorablePlace, MemorableDate, RefreshToken, RequiresPasswordReset, RefreshTokenExpiryTime, SuperiorId, Created, CreatedBy, LastModified, LastModifiedBy, UserName, NormalizedUserName, Email, NormalizedEmail, EmailConfirmed, PasswordHash, SecurityStamp, ConcurrencyStamp, PhoneNumber, PhoneNumberConfirmed, TwoFactorEnabled, LockoutEnd, LockoutEnabled, AccessFailedCount) VALUES (N'f9c42459-01e8-4ad3-8fb6-47e159d59693', N'North East Contract User', N'1.1.2.1.', N'1.1.2.1.', N'North East Contract', null, 1, 0, N'Password123!', N'123456845', null, 1, N'0001-01-01 00:00:00.0000000', null, N'2024-07-24 11:48:12.5771633', @CreatedUserId, N'2024-07-24 11:51:05.7875573', @CreatedUserId, N'[email protected]', N'[email protected]', N'[email protected]', N'[email protected]', 1, N'AQAAAAIAAYagAAAAEJNkbDpuFwA7sbXkuv972OM1M4TESjPC643BaciFBzaC7BXw4yG1Gn+BLa+VC3A0yA==', N'NVJ2GJEQUGMSXF2M5SLTMJSGTNFUFIVT', N'd3fd54de-7056-4740-af53-f5e2ef0c9eff', N'07123123256', 0, 0, null, 1, 0); | ||
INSERT INTO [Identity].[User] (Id, DisplayName, ProviderId, TenantId, TenantName, ProfilePictureDataUrl, IsActive, IsLive, MemorablePlace, MemorableDate, RefreshToken, RequiresPasswordReset, RefreshTokenExpiryTime, SuperiorId, Created, CreatedBy, LastModified, LastModifiedBy, UserName, NormalizedUserName, Email, NormalizedEmail, EmailConfirmed, PasswordHash, SecurityStamp, ConcurrencyStamp, PhoneNumber, PhoneNumberConfirmed, TwoFactorEnabled, LockoutEnd, LockoutEnabled, AccessFailedCount) VALUES (N'ffe079b0-b976-4e60-bf70-06cf2bd2b065', N'London Contract User', N'1.1.5.1.', N'1.1.5.1.', N'London Contract', null, 1, 0, N'Password123!', N'123659', null, 1, N'0001-01-01 00:00:00.0000000', null, N'2024-07-24 12:09:57.1828400', @CreatedUserId, N'2024-07-24 12:12:11.9286715', @CreatedUserId, N'[email protected]', N'[email protected]', N'[email protected]', N'[email protected]', 1, N'AQAAAAIAAYagAAAAEBNwqdcVkVZ8RzYXP+5/sp387yt5j2nNG3alHojVXNCjxxz74jcb4+e1SEzIr5FL4g==', N'JN6P4JZVSMUDBGICD6ZGDU7XII427FAB', N'2f3fdd1b-dc57-4658-b22a-0f4aa94c8320', N'07412356894', 0, 0, null, 1, 0); | ||
|
||
-- remember first time passwords to prevent them being re-used | ||
INSERT INTO [Identity].PasswordHistory ( [UserId], [PasswordHash], [CreatedAt] ) | ||
SELECT [Id], [PasswordHash], SYSUTCDATETIME() FROM [Identity].[User] | ||
WHERE [PasswordHash] IS NOT NULL; | ||
|
||
INSERT INTO [Identity].UserRole (UserId, RoleId) VALUES (N'0240afb1-78ee-497a-b42f-25f61cce5ecc', N'a6bc93b6-0c06-43e7-bc8f-8d1f1ff6f136'); | ||
INSERT INTO [Identity].UserRole (UserId, RoleId) VALUES (N'06098b9b-2a53-4bca-81fa-81e8e1befc47', N'a6bc93b6-0c06-43e7-bc8f-8d1f1ff6f136'); | ||
INSERT INTO [Identity].UserRole (UserId, RoleId) VALUES (N'08cdee0b-a6c2-4c5a-8ebe-2cff53789052', N'a6bc93b6-0c06-43e7-bc8f-8d1f1ff6f136'); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 0 additions & 4 deletions
4
src/Application/Features/Identity/Notifications/SendFactorCode/SendFactorCodeNotification.cs
This file was deleted.
Oops, something went wrong.
13 changes: 0 additions & 13 deletions
13
...ation/Features/Identity/Notifications/SendFactorCode/SendFactorCodeNotificationHandler.cs
This file was deleted.
Oops, something went wrong.
4 changes: 4 additions & 0 deletions
4
...n/Features/Identity/Notifications/SendTwoFactorCode/SendTwoFactorEmailCodeNotification.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
namespace Cfo.Cats.Application.Features.Identity.Notifications.SendTwoFactorCode; | ||
|
||
public record SendTwoFactorEmailCodeNotification(string Email, string UserName, string AuthenticatorCode) | ||
: INotification; |
13 changes: 13 additions & 0 deletions
13
...res/Identity/Notifications/SendTwoFactorCode/SendTwoFactorEmailCodeNotificationHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
namespace Cfo.Cats.Application.Features.Identity.Notifications.SendTwoFactorCode; | ||
|
||
public class SendTwoFactorEmailCodeNotificationHandler( | ||
ILogger<SendTwoFactorEmailCodeNotificationHandler> logger, | ||
ICommunicationsService communicationsService | ||
) : INotificationHandler<SendTwoFactorEmailCodeNotification> | ||
{ | ||
public async Task Handle(SendTwoFactorEmailCodeNotification notification, CancellationToken cancellationToken) | ||
{ | ||
await communicationsService.SendEmailCodeAsync(notification.Email, notification.AuthenticatorCode); | ||
logger.LogInformation("Verification Code email sent to {UserName})", notification.UserName); | ||
} | ||
} |
4 changes: 4 additions & 0 deletions
4
...on/Features/Identity/Notifications/SendTwoFactorCode/SendTwoFactorTextCodeNotification.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
namespace Cfo.Cats.Application.Features.Identity.Notifications.SendTwoFactorCode; | ||
|
||
public record SendTwoFactorTextCodeNotification(string MobileNumber, string UserName, string AuthenticatorCode) | ||
: INotification; |
11 changes: 11 additions & 0 deletions
11
...ures/Identity/Notifications/SendTwoFactorCode/SendTwoFactorTextCodeNotificationHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
namespace Cfo.Cats.Application.Features.Identity.Notifications.SendTwoFactorCode; | ||
|
||
public class SendTwoFactorTextCodeNotificationHandler(ICommunicationsService communicationsService, ILogger<SendTwoFactorTextCodeNotificationHandler> logger) | ||
: INotificationHandler<SendTwoFactorTextCodeNotification> | ||
{ | ||
public async Task Handle(SendTwoFactorTextCodeNotification notification, CancellationToken cancellationToken) | ||
{ | ||
await communicationsService.SendSmsCodeAsync(notification.MobileNumber, notification.AuthenticatorCode); | ||
logger.LogDebug("Verification Code email sent to {UserName})", notification.UserName); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace Cfo.Cats.Domain.Identity; | ||
|
||
public class PasswordHistory | ||
{ | ||
public int Id { get; set; } | ||
public string UserId { get; set; } = default!; | ||
public string PasswordHash { get; set; } = default!; | ||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
src/Infrastructure/Persistence/Configurations/Identity/PasswordHistoryConfiguration.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
using Cfo.Cats.Domain.Identity; | ||
using Cfo.Cats.Infrastructure.Constants.Database; | ||
using Microsoft.EntityFrameworkCore.Metadata.Builders; | ||
|
||
namespace Cfo.Cats.Infrastructure.Persistence.Configurations.Identity; | ||
|
||
public class PasswordHistoryConfiguration | ||
: IEntityTypeConfiguration<PasswordHistory> | ||
{ | ||
public void Configure(EntityTypeBuilder<PasswordHistory> builder) | ||
{ | ||
builder.ToTable(DatabaseConstants.Tables.PasswordHistory, DatabaseConstants.Schemas.Identity); | ||
builder.Property(ph => ph.UserId) | ||
.HasMaxLength(DatabaseConstants.FieldLengths.GuidId) | ||
.IsRequired(); | ||
|
||
builder.Property(ph => ph.PasswordHash) | ||
.IsRequired(); | ||
|
||
builder.Property(ph => ph.CreatedAt) | ||
.IsRequired(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
src/Infrastructure/Services/Identity/ApplicationUserManager.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
using Cfo.Cats.Domain.Identity; | ||
|
||
namespace Cfo.Cats.Infrastructure.Services.Identity; | ||
|
||
public class ApplicationUserManager(IUserStore<ApplicationUser> store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<ApplicationUser> passwordHasher, IEnumerable<IUserValidator<ApplicationUser>> userValidators, IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<ApplicationUserManager> logger) | ||
: UserManager<ApplicationUser>(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) | ||
{ | ||
private readonly IServiceProvider _serviceProvider = services; | ||
|
||
public override async Task<IdentityResult> ChangePasswordAsync(ApplicationUser user, string currentPassword, string newPassword) | ||
{ | ||
if (await IsPasswordInHistory(user, newPassword)) | ||
{ | ||
IdentityError error = new IdentityError() | ||
{ | ||
Code = "CATS_01", | ||
Description = "You cannot reuse your previous passwords" | ||
}; | ||
return IdentityResult.Failed(error); | ||
} | ||
|
||
var result = await base.ChangePasswordAsync(user, currentPassword, newPassword); | ||
if (result.Succeeded) | ||
{ | ||
await AddPasswordToHistory(user, user.PasswordHash!); | ||
} | ||
return result; | ||
} | ||
|
||
public override async Task<IdentityResult> ResetPasswordAsync(ApplicationUser user, string token, string newPassword) | ||
{ | ||
ThrowIfDisposed(); | ||
if (user == null) | ||
{ | ||
throw new ArgumentNullException(nameof(user)); | ||
} | ||
|
||
// Verify the reset token | ||
if (!await VerifyUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider, ResetPasswordTokenPurpose, token)) | ||
{ | ||
return IdentityResult.Failed(ErrorDescriber.InvalidToken()); | ||
} | ||
|
||
// Check password history | ||
if (await IsPasswordInHistory(user, newPassword)) | ||
{ | ||
IdentityError error = new IdentityError() | ||
{ | ||
Code = "CATS_01", | ||
Description = "You cannot reuse your previous passwords" | ||
}; | ||
return IdentityResult.Failed([error]); | ||
} | ||
|
||
// Update the password hash | ||
var result = await UpdatePasswordHash(user, newPassword, validatePassword: true); | ||
if (result.Succeeded == false) | ||
{ | ||
return result; | ||
} | ||
|
||
// Save the new password to the history | ||
await AddPasswordToHistory(user, user.PasswordHash!); | ||
|
||
// Update the user | ||
return await UpdateUserAsync(user); | ||
} | ||
|
||
private async ValueTask<bool> IsPasswordInHistory(ApplicationUser user, string newPassword) | ||
{ | ||
var passwordHasher = new PasswordHasher<ApplicationUser>(); | ||
|
||
using var scope = _serviceProvider.CreateScope(); | ||
var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>(); | ||
|
||
var passwordHistories = await uow.DbContext.PasswordHistories | ||
.AsNoTracking() | ||
.Where(ph => ph.UserId == user.Id) | ||
.ToListAsync(); | ||
|
||
foreach (var passwordHistory in passwordHistories) | ||
{ | ||
if (passwordHasher.VerifyHashedPassword(user, passwordHistory.PasswordHash, newPassword) != PasswordVerificationResult.Failed) | ||
{ | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
private async Task AddPasswordToHistory(ApplicationUser user, string userPasswordHash) | ||
{ | ||
using var scope = _serviceProvider.CreateScope(); | ||
var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>(); | ||
var passwordHistory = new PasswordHistory() | ||
{ | ||
UserId = user.Id, | ||
PasswordHash = userPasswordHash, | ||
CreatedAt = DateTime.UtcNow | ||
}; | ||
uow.DbContext.PasswordHistories.Add(passwordHistory); | ||
await uow.SaveChangesAsync(); | ||
} | ||
|
||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.