Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 56 additions & 63 deletions SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using MessagingService.Client;
using MessagingService.DataTransferObjects;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -33,7 +34,8 @@ public sealed class UserRequestHandler :
IRequestHandler<SecurityServiceCommands.ConfirmUserEmailAddressCommand, Result>,
IRequestHandler<SecurityServiceCommands.SendWelcomeEmailCommand, Result>,
IRequestHandler<SecurityServiceCommands.ProcessPasswordResetConfirmationCommand, Result<String>>,
IRequestHandler<SecurityServiceCommands.ProcessPasswordResetRequestCommand, Result>
IRequestHandler<SecurityServiceCommands.ProcessPasswordResetRequestCommand, Result>,
IRequestHandler<SecurityServiceCommands.ProcessAccountActivationCommand, Result>
{
private readonly UserManager<ApplicationUser> UserManager;
private readonly IPasswordHasher<ApplicationUser> PasswordHasher;
Expand Down Expand Up @@ -121,19 +123,18 @@ private SendEmailRequest BuildEmailConfirmationRequest(ApplicationUser user,
return request;
}

private SendEmailRequest BuildWelcomeEmail(string emailAddress,
string password)
private SendEmailRequest BuildActivationEmail(string emailAddress,
string activationUri)
{
StringBuilder mesasgeBuilder = new StringBuilder();
mesasgeBuilder.AppendLine("<html><body>");
mesasgeBuilder.AppendLine("<p>Welcome to Transaction Processing System</p>");
mesasgeBuilder.AppendLine("<p></p>");
mesasgeBuilder.AppendLine("<p>Please find below your user details:</p>");
mesasgeBuilder.AppendLine("<p>Please activate your account by choosing a password:</p>");
mesasgeBuilder.AppendLine("<table>");
mesasgeBuilder.AppendLine("<tr><td><strong>User Name</strong></td></tr>");
mesasgeBuilder.AppendLine($"<tr><td id=\"username\">{emailAddress}</td></tr>");
mesasgeBuilder.AppendLine("<tr><td><strong>Password</strong></td></tr>");
mesasgeBuilder.AppendLine($"<tr><td id=\"password\">{password}</td></tr>");
mesasgeBuilder.AppendLine($"<tr><td><a href=\"{activationUri}\">Activate account</a></td></tr>");
mesasgeBuilder.AppendLine("</table>");
mesasgeBuilder.AppendLine("</body></html>");

Expand Down Expand Up @@ -178,16 +179,18 @@ public async Task<Result> Handle(SecurityServiceCommands.CreateUserCommand comma
RegistrationDateTime = DateTime.UtcNow,
};

Result<String> passwordValueResult = String.IsNullOrEmpty(command.Password) ? PasswordGenerator.GenerateRandomPassword(this.Options.Value.PasswordOptions) : command.Password;
bool hasEmailAddress = String.IsNullOrWhiteSpace(command.EmailAddress) == false;
if (hasEmailAddress == false)
{
Result<String> passwordValueResult = String.IsNullOrEmpty(command.Password) ? PasswordGenerator.GenerateRandomPassword(this.Options.Value.PasswordOptions) : command.Password;

if (passwordValueResult.IsFailed)
return ResultHelpers.CreateFailure(passwordValueResult);
if (passwordValueResult.IsFailed)
return ResultHelpers.CreateFailure(passwordValueResult);

// Hash the default password
newIdentityUser.PasswordHash = this.PasswordHasher.HashPassword(newIdentityUser, passwordValueResult.Data);
newIdentityUser.PasswordHash = this.PasswordHasher.HashPassword(newIdentityUser, passwordValueResult.Data);

if (String.IsNullOrEmpty(newIdentityUser.PasswordHash)) {
return Result.Failure("Error generating password hash value, hash was null or empty");
if (String.IsNullOrEmpty(newIdentityUser.PasswordHash))
return Result.Failure("Error generating password hash value, hash was null or empty");
}

// Create the User
Expand All @@ -197,7 +200,7 @@ public async Task<Result> Handle(SecurityServiceCommands.CreateUserCommand comma

Result result = (createResult.IsSuccess, addRolesToUserResult.IsSuccess, addClaimsToUserResult.IsSuccess) switch
{
(true, true, true) => await SendConfirmationEmail(newIdentityUser, cancellationToken),
(true, true, true) => hasEmailAddress ? await SendConfirmationEmail(newIdentityUser, cancellationToken) : Result.Success(),
_ => await this.DeleteUser(createResult, addRolesToUserResult, addClaimsToUserResult, newIdentityUser)
};
return result; }
Expand Down Expand Up @@ -376,34 +379,7 @@ public async Task<Result> Handle(SecurityServiceCommands.SendWelcomeEmailCommand
return Result.NotFound($"No user found with username {command.Username}");
}

IdentityResult removePasswordResult = await this.UserManager.RemovePasswordAsync(user);
if (removePasswordResult.Succeeded == false)
{
return Result.Failure($"Errors removing password for user [{command.Username}]");
}

Result<string> generatedPasswordResult = PasswordGenerator.GenerateRandomPassword(this.Options.Value.PasswordOptions);
if (generatedPasswordResult.IsFailed)
{
return ResultHelpers.CreateFailure(generatedPasswordResult);
}

IdentityResult addPasswordResult = await this.UserManager.AddPasswordAsync(user, generatedPasswordResult.Data);
if (addPasswordResult.Succeeded == false)
{
return Result.Failure($"Errors adding password for user [{command.Username}]");
}

TokenResponse token = await this.GetToken();
string emailAddress = user.Email ?? user.UserName ?? string.Empty;
SendEmailRequest emailRequest = this.BuildWelcomeEmail(emailAddress, generatedPasswordResult.Data);
Result sendEmailResult = await this.MessagingServiceClient.SendEmail(token.AccessToken, emailRequest, cancellationToken);
if (sendEmailResult.IsFailed)
{
return ResultHelpers.CreateFailure(sendEmailResult);
}

return Result.Success();
return await this.SendActivationEmail(user, cancellationToken);
}

public async Task<Result> Handle(SecurityServiceCommands.ResendWelcomeEmailCommand command,
Expand All @@ -415,32 +391,43 @@ public async Task<Result> Handle(SecurityServiceCommands.ResendWelcomeEmailComma
return Result.NotFound($"No user found with username {command.Username}");
}

IdentityResult removePasswordResult = await this.UserManager.RemovePasswordAsync(user);
if (removePasswordResult.Succeeded == false)
{
return Result.Failure($"Errors removing password for user [{command.Username}]");
}
if (user.EmailConfirmed == false)
return await this.SendConfirmationEmail(user, cancellationToken);

Result<string> generatedPasswordResult = PasswordGenerator.GenerateRandomPassword(this.Options.Value.PasswordOptions);
if (generatedPasswordResult.IsFailed)
{
return ResultHelpers.CreateFailure(generatedPasswordResult);
}
return await this.SendActivationEmail(user, cancellationToken);
}

IdentityResult addPasswordResult = await this.UserManager.AddPasswordAsync(user, generatedPasswordResult.Data);
if (addPasswordResult.Succeeded == false)
{
return Result.Failure($"Errors adding password for user [{command.Username}]");
}
private async Task<Result> SendActivationEmail(ApplicationUser user, CancellationToken cancellationToken)
{
String activationToken = await this.UserManager.GeneratePasswordResetTokenAsync(user);
String emailAddress = user.Email ?? user.UserName ?? string.Empty;
String uri = QueryHelpers.AddQueryString(
$"{this.Options.Value.PublicOrigin}/Account/ActivateAccount",
new Dictionary<string, string?>
{
["userName"] = user.UserName,
["activationToken"] = activationToken
});

TokenResponse token = await this.GetToken();
string emailAddress = user.Email ?? user.UserName ?? string.Empty;
SendEmailRequest emailRequest = this.BuildWelcomeEmail(emailAddress, generatedPasswordResult.Data);
SendEmailRequest emailRequest = this.BuildActivationEmail(emailAddress, uri);
Result sendEmailResult = await this.MessagingServiceClient.SendEmail(token.AccessToken, emailRequest, cancellationToken);
if (sendEmailResult.IsFailed)
{
return ResultHelpers.CreateFailure(sendEmailResult);
}

return Result.Success();
}

public async Task<Result> Handle(SecurityServiceCommands.ProcessAccountActivationCommand command,
CancellationToken cancellationToken)
{
ApplicationUser? user = await this.UserManager.FindByNameAsync(command.Username);
if (user == null)
return Result.NotFound();

IdentityResult result = await this.UserManager.ResetPasswordAsync(user, command.Token, command.Password);
if (result.Succeeded == false)
return Result.Failure("The account activation link is invalid or has expired.");

return Result.Success();
}
Expand Down Expand Up @@ -565,8 +552,14 @@ public async Task<Result> Handle(SecurityServiceCommands.ProcessPasswordResetReq

// User has been found so send an email with reset details
String resetToken = await this.UserManager.GeneratePasswordResetTokenAsync(user);
resetToken = UrlEncoder.Default.Encode(resetToken);
String uri = $"{this.Options.Value.PublicOrigin}/Account/ResetPassword?userName={user.UserName}&resetToken={resetToken}&clientId={command.ClientId}";
String uri = QueryHelpers.AddQueryString(
$"{this.Options.Value.PublicOrigin}/Account/ResetPassword",
new Dictionary<string, string?>
{
["userName"] = user.UserName,
["resetToken"] = resetToken,
["clientId"] = command.ClientId
});

TokenResponse token = await this.GetToken();
SendEmailRequest emailRequest = this.BuildPasswordResetEmailRequest(user, uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ public record ConfirmUserEmailAddressCommand(String UserName, String ConfirmEmai

public record SendWelcomeEmailCommand(String Username) : IRequest<Result>;
public record ResendWelcomeEmailCommand(String Username) : IRequest<Result>;
public record ProcessAccountActivationCommand(String Username,
String Token,
String Password) : IRequest<Result>;
public record ProcessPasswordResetRequestCommand(String Username,
String EmailAddress,
String ClientId) : IRequest<Result>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ public static List<CreateUserRequest> ToCreateUserRequests(this DataTableRows ta
String roles = ReqnrollTableHelper.GetStringRowValue(tableRow, "Roles");

CreateUserRequest createUserRequest = new CreateUserRequest{
UserName = ReqnrollTableHelper.GetStringRowValue(tableRow, "User Name"),
EmailAddress = ReqnrollTableHelper.GetStringRowValue(tableRow, "Email Address"),
FamilyName = ReqnrollTableHelper.GetStringRowValue(tableRow, "Family Name"),
GivenName = ReqnrollTableHelper.GetStringRowValue(tableRow, "Given Name"),
Expand Down Expand Up @@ -322,4 +323,4 @@ private static List<String> SplitString(this String stringToSplit){
return result;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,10 @@ public async Task WhenIGetTheRolesRolesDetailsAreReturnedAsFollows(List<RoleResp
Result<List<UserResponse>>? user = await this.SecurityServiceClient.GetUsers(createUserRequest.EmailAddress, cancellationToken);
user.IsSuccess.ShouldBeTrue();

results.Add((createUserRequest.EmailAddress, user.Data.Single().UserId));
String userKey = String.IsNullOrWhiteSpace(createUserRequest.UserName)
? createUserRequest.EmailAddress ?? String.Empty
: createUserRequest.UserName;
results.Add((userKey, user.Data.Single().UserId));
}
return results;
}
Expand Down Expand Up @@ -341,4 +344,4 @@ public async Task WhenIGetTheUserWithUserNameTheUserDetailsAreReturnedAsFollows(
//userDetails.Data.RegistrationDateTime.Date.ShouldBe(expectedRecord.RegistrationDateTime.Date);
}

}
}
6 changes: 3 additions & 3 deletions SecurityService.IntegrationTests/Token/Token.feature
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ Background:
| merchantClient | Merchant Client | Secret1 | transactionProcessorAcl | password |

Given I create the following users
| Email Address | Password | Phone Number | Given Name | Middle Name | Family Name | Claims | Roles |
| merchantuser@testmerchant1.co.uk | 123456 | 123456789 | Test | | User 1 | EstateId:1,MerchantId:2 | Merchant |
| User Name | Email Address | Password | Phone Number | Given Name | Middle Name | Family Name | Claims | Roles |
| merchantuser | | 123456 | 123456789 | Test | | User 1 | EstateId:1,MerchantId:2 | Merchant |

@PRTest
Scenario: Get Tokens
Expand All @@ -37,7 +37,7 @@ Scenario: Get Tokens
Then my token is returned
When I request a password token with the following values
| ClientId | ClientSecret | Username | Password |
| merchantClient | Secret1 | merchantuser@testmerchant1.co.uk | 123456 |
| merchantClient | Secret1 | merchantuser | 123456 |
Then my token is returned


Loading
Loading