diff --git a/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs b/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs index d2440006..fc694dab 100644 --- a/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs +++ b/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs @@ -20,6 +20,7 @@ using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; +using Shared.Logger; using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database; namespace SecurityService.BusinessLogic.RequestHandlers; @@ -63,23 +64,12 @@ private async Task GetToken() { // Get a token to talk to the estate service String clientId = this.Options.Value.ClientId; - - //Logger.LogInformation($"Client Id is {clientId}"); - //Logger.LogInformation($"Client Secret is {clientSecret}"); if (this.TokenResponse == null) { String clientToken = this.ClientJwtService.CreateClientAssertion(clientId, this.Options.Value.IssuerUrl, 60); this.TokenResponse = TokenResponse.Create(clientToken, null, 3600, DateTimeOffset.Now, DateTimeOffset.Now.AddSeconds(3600)); - // Logger.LogInformation($"Token is {this.TokenResponse.AccessToken}"); - return this.TokenResponse; } - if (this.TokenResponse.Expires.UtcDateTime.Subtract(DateTime.UtcNow) < TimeSpan.FromMinutes(2)) - { - // Logger.LogInformation($"Token is about to expire at {this.TokenResponse.Expires.DateTime:O}"); - // Logger.LogInformation($"Token is {this.TokenResponse.AccessToken}"); - return this.TokenResponse; - } return this.TokenResponse; } @@ -94,15 +84,18 @@ private async Task SendConfirmationEmail(ApplicationUser newIdentityUser SendEmailRequest emailRequest = this.BuildEmailConfirmationRequest(newIdentityUser, uri); Result sendEmailResult = await this.MessagingServiceClient.SendEmail(token.AccessToken, emailRequest, cancellationToken); // TODO: Error handling - //if (sendEmailResult.IsFailed) - //Logger.LogWarning($"Error sending email to {newIdentityUser.Email} as part of user creation {sendEmailResult}"); + if (sendEmailResult.IsFailed) { + Logger.LogWarning($"Error sending email to {newIdentityUser.Email} as part of user creation {sendEmailResult}"); + return ResultHelpers.CreateFailure(sendEmailResult); + } + return Result.Success(); } private SendEmailRequest BuildEmailConfirmationRequest(ApplicationUser user, String emailConfirmationToken) { - StringBuilder mesasgeBuilder = new StringBuilder(); + StringBuilder mesasgeBuilder = new(); mesasgeBuilder.Append(""); mesasgeBuilder.Append(""); @@ -414,7 +407,46 @@ public async Task Handle(SecurityServiceCommands.SendWelcomeEmailCommand return Result.Success(); } - + public async Task Handle(SecurityServiceCommands.ResendWelcomeEmailCommand command, + CancellationToken cancellationToken) + { + ApplicationUser? user = await this.UserManager.FindByNameAsync(command.Username); + if (user == null) + { + 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 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(); + } + + public static class PasswordGenerator { @@ -489,17 +521,14 @@ private static void SecureShuffle(List chars) { public async Task> Handle(SecurityServiceCommands.ChangeUserPasswordCommand command, CancellationToken cancellationToken) { - //Logger.LogWarning("In Handle ChangeUserPasswordCommand"); // Find the user based on the user name passed in ApplicationUser user = await this.UserManager.FindByNameAsync(command.UserName); - if (user == null) - { - //Logger.LogWarning("In Handle ChangeUserPasswordCommand - user is null"); + if (user == null) { // this prevents giving away info to a potential hacker... return Result.NotFound(); } - //Logger.LogWarning("In Handle ChangeUserPasswordCommand - user is not null"); + IdentityResult result = await this.UserManager.ChangePasswordAsync(user, command.CurrentPassword, command.NewPassword); @@ -507,27 +536,22 @@ public async Task> Handle(SecurityServiceComman if (result.Succeeded == false) { // Log any errors - ///Logger.LogInformation($"Errors during password change for user [{command.UserName} and Client [{command.ClientId}]"); + StringBuilder errors = new StringBuilder(); foreach (IdentityError identityError in result.Errors) { - //Logger.LogInformation($"Code {identityError.Code} Description {identityError.Description}"); + errors.AppendLine($"Code {identityError.Code} Description {identityError.Description}"); } - return Result.Failure($"Errors during password change for user [{command.UserName} and Client [{command.ClientId}]"); + return Result.Failure($"Errors during password change for user [{command.UserName}] and Client [{command.ClientId}]: {errors}"); } - //Logger.LogWarning("In Handle ChangeUserPasswordCommand - password changed"); // build the redirect uri var client = await this.DbContext.ClientDefinitions.Where(c => c.ClientId == command.ClientId).SingleOrDefaultAsync(cancellationToken); - if (client == null) - { - //Logger.LogWarning("In Handle ChangeUserPasswordCommand - client not found"); - //Logger.LogInformation($"Client not found for clientId {command.ClientId}"); + if (client == null) { return Result.Invalid($"Client not found for clientId {command.ClientId}"); } - //Logger.LogWarning($"Client uri: {client.ClientUri}"); return Result.Success(new ChangeUserPasswordResult { IsSuccessful = true, RedirectUri = client.ClientUri }); } @@ -536,8 +560,7 @@ public async Task Handle(SecurityServiceCommands.ProcessPasswordResetReq // Find the user based on the user name passed in ApplicationUser user = await this.UserManager.FindByNameAsync(command.Username); - if (user == null) - { + if (user == null) { return Result.Success(); } @@ -594,7 +617,6 @@ public async Task> Handle(SecurityServiceCommands.ProcessPassword if (user == null) { // this prevents giving away info to a potential hacker... - //Logger.LogInformation($"user not found for username {command.Username}"); return Result.NotFound($"user not found for username {command.Username}"); } @@ -604,13 +626,13 @@ public async Task> Handle(SecurityServiceCommands.ProcessPassword if (result.Succeeded == false) { // Log any errors - //Logger.LogInformation($"Errors during password reset for user [{command.Username} and Client [{command.ClientId}]"); + StringBuilder errors = new StringBuilder(); foreach (IdentityError identityError in result.Errors) { - //Logger.LogInformation($"Code {identityError.Code} Description {identityError.Description}"); + errors.AppendLine($"Code {identityError.Code} Description {identityError.Description}"); } - return Result.Failure($"Errors during password reset for user [{command.Username} and Client [{command.ClientId}]"); + return Result.Failure($"Errors during password reset for user [{command.Username}] and Client [{command.ClientId}]: {errors}"); } // build the redirect uri @@ -618,7 +640,6 @@ public async Task> Handle(SecurityServiceCommands.ProcessPassword if (client == null) { - //Logger.LogInformation($"Client not found for clientId {command.ClientId}"); return Result.Invalid($"Client not found for clientId {command.ClientId}"); } diff --git a/SecurityService.BusinessLogic/Requests/SecurityServiceCommands.cs b/SecurityService.BusinessLogic/Requests/SecurityServiceCommands.cs index 02fb9ec9..edf0b21a 100644 --- a/SecurityService.BusinessLogic/Requests/SecurityServiceCommands.cs +++ b/SecurityService.BusinessLogic/Requests/SecurityServiceCommands.cs @@ -56,6 +56,7 @@ public record ChangeUserPasswordCommand(String UserName, public record ConfirmUserEmailAddressCommand(String UserName, String ConfirmEmailToken) : IRequest; public record SendWelcomeEmailCommand(String Username) : IRequest; + public record ResendWelcomeEmailCommand(String Username) : IRequest; public record ProcessPasswordResetRequestCommand(String Username, String EmailAddress, String ClientId) : IRequest; diff --git a/SecurityService.BusinessLogic/SecurityService.BusinessLogic.csproj b/SecurityService.BusinessLogic/SecurityService.BusinessLogic.csproj index 6b3cc2ce..c8e39e83 100644 --- a/SecurityService.BusinessLogic/SecurityService.BusinessLogic.csproj +++ b/SecurityService.BusinessLogic/SecurityService.BusinessLogic.csproj @@ -13,6 +13,7 @@ + diff --git a/SecurityService.BusinessLogic/SecurityServiceOptions.cs b/SecurityService.BusinessLogic/SecurityServiceOptions.cs index c4973adb..88733fe2 100644 --- a/SecurityService.BusinessLogic/SecurityServiceOptions.cs +++ b/SecurityService.BusinessLogic/SecurityServiceOptions.cs @@ -118,7 +118,6 @@ public async Task SendEmail(String accessToken, SendEmailRequest request, CancellationToken cancellationToken) { - //Logger.LogWarning($"Sending Email {request.Subject}"); this.LastEmailRequest = new SendEmailRequest() { Body = request.Body, ConnectionIdentifier = request.ConnectionIdentifier, diff --git a/SecurityService/Factories/OidcResponseFactory.cs b/SecurityService/Factories/OidcResponseFactory.cs index 71db600d..7154606e 100644 --- a/SecurityService/Factories/OidcResponseFactory.cs +++ b/SecurityService/Factories/OidcResponseFactory.cs @@ -56,50 +56,4 @@ internal static IResult TranslateResultStatus(ResultBase result) _ => Microsoft.AspNetCore.Http.Results.StatusCode((int)HttpStatusCode.NotImplemented) }; } - - //public static IResult FromResult(Result result) - //{ - // if (result.IsSuccess == false) - // { - // return TranslateResultStatus(result); - // } - - // return result.Data switch - // { - // TokenSignInResult r => Results.SignIn(r.Principal, properties: null, authenticationScheme: r.AuthenticationScheme), - // TokenForbidResult r => Results.Forbid(r.Properties, r.AuthenticationSchemes), - // TokenBadRequestResult r => Results.BadRequest(r.Error), - // _ => Results.StatusCode(500) - // }; - //} - - //public static IResult FromResult(Result result) - //{ - // if (result.IsSuccess == false) - // { - // return TranslateResultStatus(result); - // } - - // return result.Data switch - // { - // LogoutSignOutResult r => Results.SignOut(r.Properties, r.AuthenticationSchemes), - // LogoutRedirectResult r => Results.Redirect(r.Url), - // _ => Results.StatusCode(500) - // }; - //} - - //public static IResult FromResult(Result result) - //{ - // if (result.IsSuccess == false) - // { - // return TranslateResultStatus(result); - // } - - // return result.Data switch - // { - // UserInfoJsonResult r => Results.Json(r.Data), - // UserInfoChallengeResult r => Results.Challenge(r.Properties, r.AuthenticationSchemes), - // _ => Results.StatusCode(500) - // }; - //} } diff --git a/SecurityService/Pages/Account/ResendConfirmationEmail/Index.cshtml.cs b/SecurityService/Pages/Account/ResendConfirmationEmail/Index.cshtml.cs index 9db216e4..547d7c9c 100644 --- a/SecurityService/Pages/Account/ResendConfirmationEmail/Index.cshtml.cs +++ b/SecurityService/Pages/Account/ResendConfirmationEmail/Index.cshtml.cs @@ -1,19 +1,16 @@ -using System.Text; -using Microsoft.AspNetCore.Identity; +using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.AspNetCore.WebUtilities; -using SecurityService.Database; +using SecurityService.BusinessLogic.Requests; namespace SecurityService.Pages.Account.ResendConfirmationEmail; public sealed class IndexModel : PageModel { - private readonly UserManager _userManager; - - public IndexModel(UserManager userManager) - { - this._userManager = userManager; + private readonly IMediator Mediator; + + public IndexModel(IMediator mediator) { + this.Mediator = mediator; } [BindProperty] @@ -29,26 +26,15 @@ public async Task OnPostAsync(CancellationToken cancellationToken return this.Page(); } - var user = await this.ResolveUserAsync(this.Input.EmailOrUserName); - if (user is not null && string.IsNullOrWhiteSpace(user.Email) == false && user.EmailConfirmed == false) - { - var code = await this._userManager.GenerateEmailConfirmationTokenAsync(user); - var encoded = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); - var callbackUrl = this.Url.Page("/Account/ConfirmEmail/Index", null, new { userId = user.Id, code = encoded }, this.Request.Scheme) ?? $"/Account/ConfirmEmail?userId={Uri.EscapeDataString(user.Id)}&code={Uri.EscapeDataString(encoded)}"; - // TODO: Send email with callback URL - //await this._emailService.SendAsync(new AccountMessage(user.Email!, "Confirm your email", $"Confirm your account by visiting {callbackUrl}"), cancellationToken); - } + SecurityServiceCommands.ResendWelcomeEmailCommand command = new(this.Input.EmailOrUserName); + await this.Mediator.Send(command, cancellationToken); + + this.EmailQueued = true; return this.Page(); } - private async Task ResolveUserAsync(string value) - { - var user = await this._userManager.FindByEmailAsync(value); - return user ?? await this._userManager.FindByNameAsync(value); - } - public sealed class InputModel { public string EmailOrUserName { get; set; } = string.Empty; diff --git a/SecurityService/Pages/Account/ResetPassword/Index.cshtml.cs b/SecurityService/Pages/Account/ResetPassword/Index.cshtml.cs index 86db3306..6311a3f9 100644 --- a/SecurityService/Pages/Account/ResetPassword/Index.cshtml.cs +++ b/SecurityService/Pages/Account/ResetPassword/Index.cshtml.cs @@ -18,77 +18,6 @@ public sealed class IndexModel : PageModel { private const string DontMatchMessage = "Password does not match Confirm Password"; - //private readonly UserManager _userManager; - - //public IndexModel(UserManager userManager) - //{ - // this._userManager = userManager; - //} - - //[BindProperty] - //public InputModel Input { get; set; } = new(); - - //public string StatusMessage { get; private set; } = string.Empty; - - //public void OnGet(string? userId, string? code) - //{ - // if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(code)) - // { - // this.StatusMessage = "The password reset link is invalid."; - // return; - // } - - // this.Input.UserId = userId; - // this.Input.Code = code; - //} - - //public async Task OnPostAsync() - //{ - // if (string.IsNullOrWhiteSpace(this.Input.UserId) || string.IsNullOrWhiteSpace(this.Input.Code)) - // { - // this.ModelState.AddModelError(string.Empty, "The password reset request is invalid."); - // return this.Page(); - // } - - // if (string.Equals(this.Input.NewPassword, this.Input.ConfirmPassword, StringComparison.Ordinal) == false) - // { - // this.ModelState.AddModelError(string.Empty, "The password confirmation does not match."); - // return this.Page(); - // } - - // var user = await this._userManager.FindByIdAsync(this.Input.UserId); - // if (user is null) - // { - // this.StatusMessage = "The password has been reset if the account exists."; - // return this.Page(); - // } - - // var decoded = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(this.Input.Code)); - // var result = await this._userManager.ResetPasswordAsync(user, decoded, this.Input.NewPassword); - // if (result.Succeeded == false) - // { - // foreach (var error in result.Errors) - // { - // this.ModelState.AddModelError(string.Empty, error.Description); - // } - - // return this.Page(); - // } - - // this.StatusMessage = "The password has been reset. You can now sign in."; - // return this.Page(); - //} - - //public sealed class InputModel - //{ - // public string UserId { get; set; } = string.Empty; - - // public string Code { get; set; } = string.Empty; - - // public string NewPassword { get; set; } = string.Empty; - - // public string ConfirmPassword { get; set; } = string.Empty; - //} private readonly IMediator Mediator; public ViewModel View { get; set; } diff --git a/SecurityService/Program.cs b/SecurityService/Program.cs index a42f6066..5b149e87 100644 --- a/SecurityService/Program.cs +++ b/SecurityService/Program.cs @@ -60,7 +60,6 @@ var builtConfig = configBuilder.Build(); // Keep existing static usage (if you must), and initialise the ConfigurationReader now. - //builder.Configuration = builtConfig; ConfigurationReader.Initialise(builder.Configuration); // Configure Sentry on the webBuilder using the config snapshot. @@ -151,35 +150,6 @@ cookieOptions.LogoutPath = "/Account/Logout"; }); -//AuthenticationBuilder authenticationBuilder = builder.Services.AddAuthentication(); -//foreach (var provider in options.ExternalProviders.Where(provider => provider.Enabled)) -//{ -// authenticationBuilder.AddOpenIdConnect(provider.Scheme, string.IsNullOrWhiteSpace(provider.DisplayName) ? provider.Scheme : provider.DisplayName, openIdOptions => -// { -// openIdOptions.SignInScheme = IdentityConstants.ExternalScheme; -// openIdOptions.Authority = provider.Authority; -// openIdOptions.ClientId = provider.ClientId; -// openIdOptions.ClientSecret = provider.ClientSecret; -// openIdOptions.CallbackPath = provider.CallbackPath; -// openIdOptions.ResponseType = "code"; -// openIdOptions.SaveTokens = true; -// openIdOptions.GetClaimsFromUserInfoEndpoint = true; -// openIdOptions.MapInboundClaims = false; -// openIdOptions.Scope.Clear(); -// openIdOptions.Scope.Add("openid"); -// openIdOptions.Scope.Add("profile"); -// openIdOptions.Scope.Add("email"); - -// foreach (var scope in provider.Scopes.Where(scope => string.IsNullOrWhiteSpace(scope) == false).Distinct(StringComparer.OrdinalIgnoreCase)) -// { -// if (openIdOptions.Scope.Contains(scope, StringComparer.OrdinalIgnoreCase) == false) -// { -// openIdOptions.Scope.Add(scope); -// } -// } -// }); -//} - builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); diff --git a/SecurityService/SecurityService.csproj b/SecurityService/SecurityService.csproj index 86993fe6..45ad9cbf 100644 --- a/SecurityService/SecurityService.csproj +++ b/SecurityService/SecurityService.csproj @@ -8,11 +8,9 @@ - - @@ -69,6 +67,5 @@ -