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
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using MediatR;
using Microsoft.AspNetCore.Identity;
using SecurityService.BusinessLogic.Requests;
using SecurityService.Database;
using SecurityService.Models;
using SimpleResults;

namespace SecurityService.BusinessLogic.RequestHandlers;

public sealed class LoginRequestHandler :
IRequestHandler<SecurityServiceQueries.GetExternalProvidersQuery, Result<List<ExternalProviderDetails>>>,
IRequestHandler<SecurityServiceCommands.LoginCommand, Result>
{
private readonly SignInManager<ApplicationUser> _signInManager;

public LoginRequestHandler(SignInManager<ApplicationUser> signInManager)
{
this._signInManager = signInManager;
}

public async Task<Result<List<ExternalProviderDetails>>> Handle(SecurityServiceQueries.GetExternalProvidersQuery query, CancellationToken cancellationToken)
{
var providers = (await this._signInManager.GetExternalAuthenticationSchemesAsync())
.Select(scheme => new ExternalProviderDetails(scheme.Name, scheme.DisplayName ?? scheme.Name))
.OrderBy(provider => provider.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList();

return Result.Success(providers);
}

public async Task<Result> Handle(SecurityServiceCommands.LoginCommand command, CancellationToken cancellationToken)
{
var result = await this._signInManager.PasswordSignInAsync(command.Username, command.Password, command.RememberLogin, lockoutOnFailure: true);

if (result.Succeeded)
{
return Result.Success();
}

if (result.IsLockedOut)
{
return Result.Failure("Your account has been locked. Please try again later or contact support.");
}

if (result.IsNotAllowed)
{
return Result.Failure("You are not allowed to sign in. Please confirm your email address.");
}

if (result.RequiresTwoFactor)
{
return Result.Failure("Two-factor authentication is required.");
}

return Result.Failure("Invalid username or password.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,6 @@ public record ProcessPasswordResetConfirmationCommand(String Username,
String Token,
String Password,
String ClientId) : IRequest<Result<String>>;

public sealed record LoginCommand(string Username, string Password, bool RememberLogin) : IRequest<Result>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ public sealed record GetRoleQuery(string RoleId) : IRequest<Result<RoleDetails>>
public sealed record GetRolesQuery() : IRequest<Result<List<RoleDetails>>>;
public sealed record GetUserQuery(string UserId) : IRequest<Result<UserDetails>>;
public sealed record GetUsersQuery(string? UserName) : IRequest<Result<List<UserDetails>>>;
public sealed record GetExternalProvidersQuery() : IRequest<Result<List<ExternalProviderDetails>>>;
}
2 changes: 2 additions & 0 deletions SecurityService.Models/DetailsModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ public sealed record UserDetails(
string? FamilyName,
IReadOnlyDictionary<string, string> Claims,
IReadOnlyCollection<string> Roles);

public sealed record ExternalProviderDetails(string Name, string DisplayName);
25 changes: 13 additions & 12 deletions SecurityService.UnitTests/Pages/LoginPageModelTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Moq;
using SecurityService.Database;
using SecurityService.BusinessLogic.Requests;
using SecurityService.Models;
using SecurityService.UnitTests.Infrastructure;
using Shouldly;
using SimpleResults;

namespace SecurityService.UnitTests.Pages;

Expand All @@ -13,9 +15,11 @@ public class LoginPageModelTests
[Fact]
public async Task OnPost_WhenForgotPasswordSelected_RedirectsToForgotPasswordWithReturnUrlAndClientId()
{
var userManager = IdentityMocks.CreateUserManager();
var signInManager = IdentityMocks.CreateSignInManager(userManager);
var model = CreateModel(signInManager, new DefaultHttpContext());
var mediator = new Mock<IMediator>();
mediator.Setup(m => m.Send(It.IsAny<SecurityServiceQueries.GetExternalProvidersQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Success(new List<ExternalProviderDetails>()));

var model = CreateModel(mediator, new DefaultHttpContext());
model.Input = new SecurityService.Pages.Account.Login.IndexModel.InputModel
{
Button = "forgotpassword",
Expand All @@ -30,16 +34,12 @@ public async Task OnPost_WhenForgotPasswordSelected_RedirectsToForgotPasswordWit
redirect.RouteValues.ShouldNotBeNull();
redirect.RouteValues["returnUrl"].ShouldBe("/connect/authorize?client_id=test-client-id");
redirect.RouteValues["clientId"].ShouldBe("test-client-id");
signInManager.Verify(instance => instance.PasswordSignInAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<bool>(),
It.IsAny<bool>()), Times.Never);
mediator.Verify(m => m.Send(It.IsAny<SecurityServiceCommands.LoginCommand>(), It.IsAny<CancellationToken>()), Times.Never);
}

private static SecurityService.Pages.Account.Login.IndexModel CreateModel(Mock<SignInManager<ApplicationUser>> signInManager, HttpContext httpContext)
private static SecurityService.Pages.Account.Login.IndexModel CreateModel(Mock<IMediator> mediator, HttpContext httpContext)
{
return new SecurityService.Pages.Account.Login.IndexModel(signInManager.Object)
return new SecurityService.Pages.Account.Login.IndexModel(mediator.Object)
{
PageContext = new Microsoft.AspNetCore.Mvc.RazorPages.PageContext
{
Expand All @@ -48,3 +48,4 @@ private static SecurityService.Pages.Account.Login.IndexModel CreateModel(Mock<S
};
}
}

25 changes: 13 additions & 12 deletions SecurityService/Pages/Account/Login/Index.cshtml.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
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.Login;

public sealed class IndexModel : PageModel
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IMediator _mediator;

public IndexModel(SignInManager<ApplicationUser> signInManager)
public IndexModel(IMediator mediator)
{
this._signInManager = signInManager;
this._mediator = mediator;
}

[BindProperty]
Expand Down Expand Up @@ -52,22 +52,22 @@ public async Task<IActionResult> OnPostAsync()
});
}

var result = await this._signInManager.PasswordSignInAsync(this.Input.Username, this.Input.Password, this.Input.RememberLogin, lockoutOnFailure: true);
if (result.Succeeded)
var result = await this._mediator.Send(new SecurityServiceCommands.LoginCommand(this.Input.Username, this.Input.Password, this.Input.RememberLogin));
if (result.IsSuccess)
{
return this.LocalRedirect(string.IsNullOrWhiteSpace(this.Input.ReturnUrl) ? "/" : this.Input.ReturnUrl);
}

this.ModelState.AddModelError(string.Empty, "Invalid username or password.");
this.ModelState.AddModelError(string.Empty, result.Errors.FirstOrDefault() ?? result.Message ?? "Invalid username or password.");
return this.Page();
}

private async Task LoadProvidersAsync()
{
this.Providers = (await this._signInManager.GetExternalAuthenticationSchemesAsync())
.Select(scheme => new ExternalProviderViewModel(scheme.Name, scheme.DisplayName ?? scheme.Name))
.OrderBy(provider => provider.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToArray();
var result = await this._mediator.Send(new SecurityServiceQueries.GetExternalProvidersQuery());
this.Providers = result.IsSuccess
? result.Data.Select(p => new ExternalProviderViewModel(p.Name, p.DisplayName)).ToArray()
: Array.Empty<ExternalProviderViewModel>();
}

public sealed class InputModel
Expand All @@ -83,3 +83,4 @@ public sealed class InputModel

public sealed record ExternalProviderViewModel(string Name, string DisplayName);
}

Loading