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
Expand Up @@ -40,8 +40,18 @@ this IEndpointRouteBuilder endpoints

accountGroup.MapGet(
"/SignOutAndRedirect",
async ([FromServices] SignInManager<User> signInManager) =>
async (HttpContext context, [FromServices] SignInManager<User> signInManager) =>
{
// This GET sign-out exists because an interactive Blazor circuit cannot
// write the sign-out cookie itself (the DeletePersonalData flow force-
// navigates here). Reject cross-site requests so a third-party page cannot
// force a sign-out via <img>/link/prefetch (logout CSRF, #A-1). A same-
// origin top-level navigation reports Sec-Fetch-Site: same-origin; a
// cross-site request reports cross-site. Fail closed without signing out.
var site = context.Request.Headers["Sec-Fetch-Site"].FirstOrDefault();
if (string.Equals(site, "cross-site", StringComparison.OrdinalIgnoreCase))
return TypedResults.LocalRedirect("~/");

await signInManager.SignOutAsync();
return TypedResults.LocalRedirect("~/");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@inject UserManager<User> UserManager
@inject SignInManager<User> SignInManager
@inject IPasswordHasher<User> PasswordHasher
@inject ILogger<Login> Logger
@inject NavigationManager NavigationManager
@inject NotificationService NotificationService
Expand Down Expand Up @@ -57,7 +58,18 @@
{
var user = await UserManager.FindByEmailAsync(model.Email);

if (user == null || !(await SignInManager.CanSignInAsync(user)))
if (user is null)
{
// Consume comparable time to a real password verification so a non-existent
// account is not distinguishable from a wrong password by response latency
// (user enumeration, CWE-208). The uniform message already hides it in the
// response body; this closes the timing side channel.
_ = PasswordHasher.HashPassword(new User(), model.Password);
NotificationService.Notify(NotificationSeverity.Error, "LoginError", "Invalid login attempt.");
return;
}

if (!await SignInManager.CanSignInAsync(user))
{
NotificationService.Notify(NotificationSeverity.Error, "LoginError", "Invalid login attempt.");
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ private static SignInManager<User> BuildSignInManager(UserManager<User> userMana
Services.AddSingleton(new NotificationService());
Services.AddSingleton(Substitute.For<ILogger<Login>>());
Services.AddSingleton(new LoginTokenProtector(DataProtectionProvider.Create("Tests")));
Services.AddSingleton<IPasswordHasher<User>>(new PasswordHasher<User>());
var cut = Render<LoginForm>();
return (cut, um);
}
Expand Down