diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml
index ddb1f9a2..9863af53 100644
--- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml
+++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml
@@ -29,6 +29,9 @@
@Html.DisplayNameFor(m => m.Input.RememberMe)
+
@@ -82,5 +85,6 @@
@section Scripts {
+
}
diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs
index a3ea80a7..5bb5f29f 100644
--- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs
+++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs
@@ -1,15 +1,19 @@
using System.ComponentModel.DataAnnotations;
using EssentialCSharp.Web.Areas.Identity.Data;
+using EssentialCSharp.Web.Models;
+using EssentialCSharp.Web.Services;
using EssentialCSharp.Web.Services.Referrals;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Options;
namespace EssentialCSharp.Web.Areas.Identity.Pages.Account;
-public class LoginModel(SignInManager signInManager, UserManager userManager, ILogger logger, IReferralService referralService) : PageModel
+public class LoginModel(SignInManager signInManager, UserManager userManager, ILogger logger, IReferralService referralService, ICaptchaService captchaService, IOptions optionsAccessor) : PageModel
{
+ public CaptchaOptions CaptchaOptions { get; } = optionsAccessor.Value;
private InputModel? _Input;
[BindProperty]
public InputModel Input
@@ -60,49 +64,119 @@ public async Task OnGetAsync(string? returnUrl = null)
public async Task OnPostAsync(string? returnUrl = null)
{
returnUrl ??= Url.Content("~/");
+ string? hCaptcha_response = Request.Form[CaptchaOptions.HttpPostResponseKeyName];
ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
- if (ModelState.IsValid)
+ if (!ModelState.IsValid)
{
- Microsoft.AspNetCore.Identity.SignInResult result;
- if (Input.Email is null)
- {
- return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl });
- }
- EssentialCSharpWebUser? foundUser = await userManager.FindByEmailAsync(Input.Email);
- if (Input.Password is null)
- {
- return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl });
- }
- if (foundUser is not null)
- {
- result = await signInManager.PasswordSignInAsync(foundUser, Input.Password, Input.RememberMe, lockoutOnFailure: true);
- // Call the referral service to get the referral ID and set it onto the user claim
- _ = await referralService.EnsureReferralIdAsync(foundUser);
- }
- else
- {
- result = await signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
- }
- if (result.Succeeded)
- {
- logger.LogInformation("User logged in.");
- return LocalRedirect(returnUrl);
- }
- if (result.RequiresTwoFactor)
- {
- return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
- }
- if (result.IsLockedOut)
+ return Page();
+ }
+
+ if (hCaptcha_response is null)
+ {
+ ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, HCaptchaErrorDetails.GetValue(HCaptchaErrorDetails.MissingInputResponse).FriendlyDescription);
+ return Page();
+ }
+
+ HCaptchaResult? response = await captchaService.VerifyAsync(hCaptcha_response);
+ if (response is null)
+ {
+ ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, "Error: HCaptcha API response unexpectedly null");
+ return Page();
+ }
+
+ if (response.Success)
+ {
+ if (ModelState.IsValid)
{
- logger.LogWarning("User account locked out.");
- return RedirectToPage("./Lockout");
+ Microsoft.AspNetCore.Identity.SignInResult result;
+ if (Input.Email is null)
+ {
+ return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl });
+ }
+ EssentialCSharpWebUser? foundUser = await userManager.FindByEmailAsync(Input.Email);
+ if (Input.Password is null)
+ {
+ return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl });
+ }
+ if (foundUser is not null)
+ {
+ result = await signInManager.PasswordSignInAsync(foundUser, Input.Password, Input.RememberMe, lockoutOnFailure: true);
+ // Call the referral service to get the referral ID and set it onto the user claim
+ _ = await referralService.EnsureReferralIdAsync(foundUser);
+ }
+ else
+ {
+ result = await signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
+ }
+ if (result.Succeeded)
+ {
+ logger.LogInformation("User logged in.");
+ return LocalRedirect(returnUrl);
+ }
+ if (result.RequiresTwoFactor)
+ {
+ return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
+ }
+ if (result.IsLockedOut)
+ {
+ logger.LogWarning("User account locked out.");
+ return RedirectToPage("./Lockout");
+ }
+ else
+ {
+ ModelState.AddModelError(string.Empty, "Invalid login attempt.");
+ return Page();
+ }
}
- else
+ }
+ else
+ {
+ switch (response.ErrorCodes?.Length)
{
- ModelState.AddModelError(string.Empty, "Invalid login attempt.");
- return Page();
+ case 0:
+ throw new InvalidOperationException("The HCaptcha determined the passcode is not valid, and does not meet the security criteria");
+ case > 1:
+ throw new InvalidOperationException("HCaptcha returned error codes: " + string.Join(", ", response.ErrorCodes));
+ default:
+ {
+ if (response.ErrorCodes is null)
+ {
+ throw new InvalidOperationException("HCaptcha returned error codes unexpectedly null");
+ }
+ if (HCaptchaErrorDetails.TryGetValue(response.ErrorCodes.Single(), out HCaptchaErrorDetails? details))
+ {
+ switch (details.ErrorCode)
+ {
+ case HCaptchaErrorDetails.MissingInputResponse:
+ case HCaptchaErrorDetails.InvalidInputResponse:
+ case HCaptchaErrorDetails.InvalidOrAlreadySeenResponse:
+ ModelState.AddModelError(string.Empty, details.FriendlyDescription);
+ logger.LogInformation("HCaptcha returned error code: {ErrorDetails}", details.ToString());
+ break;
+ case HCaptchaErrorDetails.BadRequest:
+ ModelState.AddModelError(string.Empty, details.FriendlyDescription);
+ logger.LogInformation("HCaptcha returned error code: {ErrorDetails}", details.ToString());
+ break;
+ case HCaptchaErrorDetails.MissingInputSecret:
+ case HCaptchaErrorDetails.InvalidInputSecret:
+ case HCaptchaErrorDetails.NotUsingDummyPasscode:
+ case HCaptchaErrorDetails.SitekeySecretMismatch:
+ logger.LogCritical("HCaptcha returned error code: {ErrorDetails}", details.ToString());
+ break;
+ default:
+ throw new InvalidOperationException("HCaptcha returned unknown error code: " + details?.ErrorCode);
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException("HCaptcha returned unknown error code: " + response.ErrorCodes.Single());
+ }
+
+ break;
+ }
+
}
}
diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml
index d457b6b8..ce2c304f 100644
--- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml
+++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml
@@ -16,11 +16,15 @@
+
@section Scripts {
+
}
diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs
index 8ef99283..15c4400e 100644
--- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs
+++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs
@@ -2,18 +2,22 @@
using System.Text;
using System.Text.Encodings.Web;
using EssentialCSharp.Web.Areas.Identity.Data;
+using EssentialCSharp.Web.Models;
+using EssentialCSharp.Web.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Options;
namespace EssentialCSharp.Web.Areas.Identity.Pages.Account;
[AllowAnonymous]
-public class ResendEmailConfirmationModel(UserManager userManager, IEmailSender emailSender) : PageModel
+public class ResendEmailConfirmationModel(UserManager userManager, IEmailSender emailSender, ICaptchaService captchaService, IOptions optionsAccessor) : PageModel
{
+ public CaptchaOptions CaptchaOptions { get; } = optionsAccessor.Value;
private InputModel? _Input;
[BindProperty]
public InputModel Input
@@ -31,43 +35,108 @@ public class InputModel
public async Task OnPostAsync()
{
+ string? hCaptcha_response = Request.Form[CaptchaOptions.HttpPostResponseKeyName];
+
if (!ModelState.IsValid)
{
return Page();
}
- if (Input.Email is null)
+ if (hCaptcha_response is null)
{
- ModelState.AddModelError(string.Empty, "Error: Email is null. Please enter in an email");
+ ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, HCaptchaErrorDetails.GetValue(HCaptchaErrorDetails.MissingInputResponse).FriendlyDescription);
return Page();
}
- EssentialCSharpWebUser? user = await userManager.FindByEmailAsync(Input.Email);
- if (user is null)
+ HCaptchaResult? response = await captchaService.VerifyAsync(hCaptcha_response);
+ if (response is null)
{
- return NotFound($"Unable to load user with ID '{userManager.GetUserId(User)}'.");
+ ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, "Error: HCaptcha API response unexpectedly null");
+ return Page();
}
- string userId = await userManager.GetUserIdAsync(user);
- string code = await userManager.GenerateEmailConfirmationTokenAsync(user);
- code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
- string? callbackUrl = Url.Page(
- "/Account/ConfirmEmail",
- pageHandler: null,
- values: new { userId = userId, code = code },
- protocol: Request.Scheme);
-
- if (callbackUrl is null)
+ if (response.Success)
{
- ModelState.AddModelError(string.Empty, "Error: callback url unexpectedly null.");
+ if (Input.Email is null)
+ {
+ ModelState.AddModelError(string.Empty, "Error: Email is null. Please enter in an email");
+ return Page();
+ }
+
+ EssentialCSharpWebUser? user = await userManager.FindByEmailAsync(Input.Email);
+ if (user is null)
+ {
+ return NotFound($"Unable to load user with ID '{userManager.GetUserId(User)}'.");
+ }
+
+ string userId = await userManager.GetUserIdAsync(user);
+ string code = await userManager.GenerateEmailConfirmationTokenAsync(user);
+ code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
+ string? callbackUrl = Url.Page(
+ "/Account/ConfirmEmail",
+ pageHandler: null,
+ values: new { userId = userId, code = code },
+ protocol: Request.Scheme);
+
+ if (callbackUrl is null)
+ {
+ ModelState.AddModelError(string.Empty, "Error: callback url unexpectedly null.");
+ return Page();
+ }
+ await emailSender.SendEmailAsync(
+ Input.Email,
+ "Confirm your email",
+ $"Please confirm your account by clicking here.");
+
+ ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email. If you can't find the email, please check your spam folder.");
return Page();
}
- await emailSender.SendEmailAsync(
- Input.Email,
- "Confirm your email",
- $"Please confirm your account by clicking here.");
+ else
+ {
+ switch (response.ErrorCodes?.Length)
+ {
+ case 0:
+ throw new InvalidOperationException("The HCaptcha determined the passcode is not valid, and does not meet the security criteria");
+ case > 1:
+ throw new InvalidOperationException("HCaptcha returned error codes: " + string.Join(", ", response.ErrorCodes));
+ default:
+ {
+ if (response.ErrorCodes is null)
+ {
+ throw new InvalidOperationException("HCaptcha returned error codes unexpectedly null");
+ }
+ if (HCaptchaErrorDetails.TryGetValue(response.ErrorCodes.Single(), out HCaptchaErrorDetails? details))
+ {
+ switch (details.ErrorCode)
+ {
+ case HCaptchaErrorDetails.MissingInputResponse:
+ case HCaptchaErrorDetails.InvalidInputResponse:
+ case HCaptchaErrorDetails.InvalidOrAlreadySeenResponse:
+ ModelState.AddModelError(string.Empty, details.FriendlyDescription);
+ break;
+ case HCaptchaErrorDetails.BadRequest:
+ ModelState.AddModelError(string.Empty, details.FriendlyDescription);
+ break;
+ case HCaptchaErrorDetails.MissingInputSecret:
+ case HCaptchaErrorDetails.InvalidInputSecret:
+ case HCaptchaErrorDetails.NotUsingDummyPasscode:
+ case HCaptchaErrorDetails.SitekeySecretMismatch:
+ break;
+ default:
+ throw new InvalidOperationException("HCaptcha returned unknown error code: " + details?.ErrorCode);
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException("HCaptcha returned unknown error code: " + response.ErrorCodes.Single());
+ }
+
+ break;
+ }
+
+ }
+ }
- ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email. If you can't find the email, please check your spam folder.");
return Page();
}
}
diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml
index e430d01e..23749f1d 100644
--- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml
+++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml
@@ -27,11 +27,15 @@
+
@section Scripts {
+
}
diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs
index 383de416..dcc1e30a 100644
--- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs
+++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs
@@ -1,15 +1,19 @@
using System.ComponentModel.DataAnnotations;
using System.Text;
using EssentialCSharp.Web.Areas.Identity.Data;
+using EssentialCSharp.Web.Models;
+using EssentialCSharp.Web.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Options;
namespace EssentialCSharp.Web.Areas.Identity.Pages.Account;
-public class ResetPasswordModel(UserManager userManager) : PageModel
+public class ResetPasswordModel(UserManager userManager, ICaptchaService captchaService, IOptions optionsAccessor) : PageModel
{
+ public CaptchaOptions CaptchaOptions { get; } = optionsAccessor.Value;
private InputModel? _Input;
[BindProperty]
public InputModel Input
@@ -57,44 +61,109 @@ public IActionResult OnGet(string? code = null)
public async Task OnPostAsync()
{
+ string? hCaptcha_response = Request.Form[CaptchaOptions.HttpPostResponseKeyName];
+
if (!ModelState.IsValid)
{
return Page();
}
- if (Input.Email is null)
+ if (hCaptcha_response is null)
{
- ModelState.AddModelError(string.Empty, "Error: Email is required.");
- return RedirectToPage();
+ ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, HCaptchaErrorDetails.GetValue(HCaptchaErrorDetails.MissingInputResponse).FriendlyDescription);
+ return Page();
}
- EssentialCSharpWebUser? user = await userManager.FindByEmailAsync(Input.Email);
- if (user is null)
+
+ HCaptchaResult? response = await captchaService.VerifyAsync(hCaptcha_response);
+ if (response is null)
{
- // Don't reveal that the user does not exist
- return RedirectToPage("./ResetPasswordConfirmation");
+ ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, "Error: HCaptcha API response unexpectedly null");
+ return Page();
}
- if (Input.Password is null)
+ if (response.Success)
{
- ModelState.AddModelError(string.Empty, "Error: Password is required.");
- return RedirectToPage();
+ if (Input.Email is null)
+ {
+ ModelState.AddModelError(string.Empty, "Error: Email is required.");
+ return RedirectToPage();
+ }
+ EssentialCSharpWebUser? user = await userManager.FindByEmailAsync(Input.Email);
+ if (user is null)
+ {
+ // Don't reveal that the user does not exist
+ return RedirectToPage("./ResetPasswordConfirmation");
+ }
+
+ if (Input.Password is null)
+ {
+ ModelState.AddModelError(string.Empty, "Error: Password is required.");
+ return RedirectToPage();
+ }
+ if (Input.Code is null)
+ {
+ ModelState.AddModelError(string.Empty, "Error: Code is required.");
+ return RedirectToPage();
+ }
+
+ IdentityResult result = await userManager.ResetPasswordAsync(user, Input.Code, Input.Password);
+ if (result.Succeeded)
+ {
+ return RedirectToPage("./ResetPasswordConfirmation");
+ }
+
+ foreach (IdentityError error in result.Errors)
+ {
+ ModelState.AddModelError(string.Empty, error.Description);
+ }
+ return Page();
}
- if (Input.Code is null)
+ else
{
- ModelState.AddModelError(string.Empty, "Error: Code is required.");
- return RedirectToPage();
- }
+ switch (response.ErrorCodes?.Length)
+ {
+ case 0:
+ throw new InvalidOperationException("The HCaptcha determined the passcode is not valid, and does not meet the security criteria");
+ case > 1:
+ throw new InvalidOperationException("HCaptcha returned error codes: " + string.Join(", ", response.ErrorCodes));
+ default:
+ {
+ if (response.ErrorCodes is null)
+ {
+ throw new InvalidOperationException("HCaptcha returned error codes unexpectedly null");
+ }
+ if (HCaptchaErrorDetails.TryGetValue(response.ErrorCodes.Single(), out HCaptchaErrorDetails? details))
+ {
+ switch (details.ErrorCode)
+ {
+ case HCaptchaErrorDetails.MissingInputResponse:
+ case HCaptchaErrorDetails.InvalidInputResponse:
+ case HCaptchaErrorDetails.InvalidOrAlreadySeenResponse:
+ ModelState.AddModelError(string.Empty, details.FriendlyDescription);
+ break;
+ case HCaptchaErrorDetails.BadRequest:
+ ModelState.AddModelError(string.Empty, details.FriendlyDescription);
+ break;
+ case HCaptchaErrorDetails.MissingInputSecret:
+ case HCaptchaErrorDetails.InvalidInputSecret:
+ case HCaptchaErrorDetails.NotUsingDummyPasscode:
+ case HCaptchaErrorDetails.SitekeySecretMismatch:
+ break;
+ default:
+ throw new InvalidOperationException("HCaptcha returned unknown error code: " + details?.ErrorCode);
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException("HCaptcha returned unknown error code: " + response.ErrorCodes.Single());
+ }
- IdentityResult result = await userManager.ResetPasswordAsync(user, Input.Code, Input.Password);
- if (result.Succeeded)
- {
- return RedirectToPage("./ResetPasswordConfirmation");
- }
+ break;
+ }
- foreach (IdentityError error in result.Errors)
- {
- ModelState.AddModelError(string.Empty, error.Description);
+ }
}
+
return Page();
}
}