diff --git a/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs b/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs index 30cbeea..5bbd078 100644 --- a/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs +++ b/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; @@ -43,12 +44,14 @@ public sealed class UserRequestHandler : private readonly IMessagingServiceClient MessagingServiceClient; private readonly IClientJwtService ClientJwtService; private readonly SecurityServiceDbContext DbContext; + private readonly IHostEnvironment HostEnvironment; public UserRequestHandler(UserManager userManager, IPasswordHasher passwordHasher, IOptions options, IMessagingServiceClient messagingServiceClient, IClientJwtService clientJwtService, - SecurityServiceDbContext dbContext) { + SecurityServiceDbContext dbContext, + IHostEnvironment hostEnvironment) { @@ -58,6 +61,7 @@ public UserRequestHandler(UserManager userManager, IPasswordHas this.MessagingServiceClient = messagingServiceClient; this.ClientJwtService = clientJwtService; this.DbContext = dbContext; + this.HostEnvironment = hostEnvironment; } private TokenResponse TokenResponse; @@ -180,7 +184,10 @@ public async Task Handle(SecurityServiceCommands.CreateUserCommand comma }; bool hasEmailAddress = String.IsNullOrWhiteSpace(command.EmailAddress) == false; - if (hasEmailAddress == false) + bool shouldUseProvidedPassword = hasEmailAddress == false || + (this.HostEnvironment.IsEnvironment("IntegrationTest") && + this.Options.Value.UserOptions.UseProvidedPasswordForEmailUsers); + if (shouldUseProvidedPassword) { Result passwordValueResult = String.IsNullOrEmpty(command.Password) ? PasswordGenerator.GenerateRandomPassword(this.Options.Value.PasswordOptions) : command.Password; diff --git a/SecurityService.BusinessLogic/SecurityServiceOptions.cs b/SecurityService.BusinessLogic/SecurityServiceOptions.cs index 9c27309..97faa64 100644 --- a/SecurityService.BusinessLogic/SecurityServiceOptions.cs +++ b/SecurityService.BusinessLogic/SecurityServiceOptions.cs @@ -109,6 +109,8 @@ public class UserOptions public Boolean RequireUniqueEmail { get; set; } + public Boolean UseProvidedPasswordForEmailUsers { get; set; } + #endregion } diff --git a/SecurityService.IntegrationTests/Common/DockerHelper.cs b/SecurityService.IntegrationTests/Common/DockerHelper.cs index 8a1636f..ecfb28b 100644 --- a/SecurityService.IntegrationTests/Common/DockerHelper.cs +++ b/SecurityService.IntegrationTests/Common/DockerHelper.cs @@ -81,6 +81,7 @@ public override ContainerBuilder SetupSecurityServiceContainer() environmentVariables.Add("ServiceOptions:PasswordOptions:RequireDigit", "false"); environmentVariables.Add("ServiceOptions:PasswordOptions:RequireUpperCase", "false"); environmentVariables.Add("ServiceOptions:UserOptions:RequireUniqueEmail", "false"); + environmentVariables.Add("ServiceOptions:UserOptions:UseProvidedPasswordForEmailUsers", "true"); environmentVariables.Add("ServiceOptions:SignInOptions:RequireConfirmedEmail", "false"); environmentVariables.Add("ServiceOptions:ManagementBootstrap:Enabled", "true"); environmentVariables.Add("ServiceOptions:ManagementBootstrap:ClientId", "management-bootstrap"); diff --git a/SecurityService.OpenIdConnect.IntegrationTests/Common/DockerHelper.cs b/SecurityService.OpenIdConnect.IntegrationTests/Common/DockerHelper.cs index 9460d76..99dd7c9 100644 --- a/SecurityService.OpenIdConnect.IntegrationTests/Common/DockerHelper.cs +++ b/SecurityService.OpenIdConnect.IntegrationTests/Common/DockerHelper.cs @@ -271,6 +271,7 @@ public override ContainerBuilder SetupSecurityServiceContainer() environmentVariables.Add($"ServiceOptions:PasswordOptions:RequireDigit","false"); environmentVariables.Add($"ServiceOptions:PasswordOptions:RequireUpperCase","false"); environmentVariables.Add($"ServiceOptions:UserOptions:RequireUniqueEmail","false"); + environmentVariables.Add("ServiceOptions:UserOptions:UseProvidedPasswordForEmailUsers", "true"); environmentVariables.Add($"ServiceOptions:SignInOptions:RequireConfirmedEmail","false"); environmentVariables.Add("ServiceOptions:ManagementBootstrap:Enabled", "true"); environmentVariables.Add("ServiceOptions:ManagementBootstrap:ClientId", "management-bootstrap"); diff --git a/SecurityService.UnitTests/Infrastructure/TestServiceProviderFactory.cs b/SecurityService.UnitTests/Infrastructure/TestServiceProviderFactory.cs index 86a3a70..8b52cb9 100644 --- a/SecurityService.UnitTests/Infrastructure/TestServiceProviderFactory.cs +++ b/SecurityService.UnitTests/Infrastructure/TestServiceProviderFactory.cs @@ -5,6 +5,8 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Internal; using Microsoft.IdentityModel.Tokens; using SecurityService.BusinessLogic; using SecurityService.BusinessLogic.Requests; @@ -22,7 +24,8 @@ public static ServiceProvider Create( Action? configureOptions = null, TimeSpan? passwordTokenLifespan = null, bool useSqlite = false, - SaveChangesInterceptor? saveChangesInterceptor = null) + SaveChangesInterceptor? saveChangesInterceptor = null, + string environmentName = "UnitTest") { var rsa = RSA.Create(2048); var key = new RsaSecurityKey(rsa); @@ -39,6 +42,11 @@ public static ServiceProvider Create( configureOptions?.Invoke(options); }); services.AddSingleton(); + services.AddSingleton(new HostingEnvironment + { + EnvironmentName = environmentName, + ApplicationName = typeof(TestServiceProviderFactory).Assembly.GetName().Name + }); services.AddLogging(); services.AddDataProtection().UseEphemeralDataProtectionProvider(); if (passwordTokenLifespan.HasValue) diff --git a/SecurityService.UnitTests/RequestHandlers/UserRequestHandlerTests.cs b/SecurityService.UnitTests/RequestHandlers/UserRequestHandlerTests.cs index 6426576..76226a5 100644 --- a/SecurityService.UnitTests/RequestHandlers/UserRequestHandlerTests.cs +++ b/SecurityService.UnitTests/RequestHandlers/UserRequestHandlerTests.cs @@ -36,6 +36,48 @@ public async Task CreateUser_WithEmailAddress_DoesNotCreateAnInitialPassword() user.PasswordHash.ShouldBeNull(); } + [Fact] + public async Task CreateUser_WithEmailAddressInIntegrationTestMode_UsesProvidedPasswordWhenEnabled() + { + using var provider = TestServiceProviderFactory.Create( + nameof(this.CreateUser_WithEmailAddressInIntegrationTestMode_UsesProvidedPasswordWhenEnabled), + configureOptions: options => options.UserOptions.UseProvidedPasswordForEmailUsers = true, + environmentName: "IntegrationTest"); + var mediator = provider.GetRequiredService(); + + var result = await mediator.Send(new SecurityServiceCommands.CreateUserCommand( + "Alice", null, "Tester", "alice", "SuppliedPassword1!", "alice@example.com", null, + new Dictionary(), new List())); + + result.IsSuccess.ShouldBeTrue(); + using var scope = provider.CreateScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + var user = await userManager.FindByNameAsync("alice"); + user.ShouldNotBeNull(); + (await userManager.CheckPasswordAsync(user, "SuppliedPassword1!")).ShouldBeTrue(); + } + + [Fact] + public async Task CreateUser_WithEmailAddressOutsideIntegrationTestMode_IgnoresProvidedPassword() + { + using var provider = TestServiceProviderFactory.Create( + nameof(this.CreateUser_WithEmailAddressOutsideIntegrationTestMode_IgnoresProvidedPassword), + configureOptions: options => options.UserOptions.UseProvidedPasswordForEmailUsers = true, + environmentName: "Development"); + var mediator = provider.GetRequiredService(); + + var result = await mediator.Send(new SecurityServiceCommands.CreateUserCommand( + "Alice", null, "Tester", "alice", "SuppliedPassword1!", "alice@example.com", null, + new Dictionary(), new List())); + + result.IsSuccess.ShouldBeTrue(); + using var scope = provider.CreateScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + var user = await userManager.FindByNameAsync("alice"); + user.ShouldNotBeNull(); + user.PasswordHash.ShouldBeNull(); + } + [Fact] public async Task UserLifecycle_CreateGetAndList_Works() {