|
1 | 1 | using System.ComponentModel.DataAnnotations; |
2 | 2 | using EssentialCSharp.Web.Areas.Identity.Data; |
| 3 | +using EssentialCSharp.Web.Models; |
| 4 | +using EssentialCSharp.Web.Services; |
3 | 5 | using EssentialCSharp.Web.Services.Referrals; |
4 | 6 | using Microsoft.AspNetCore.Authentication; |
5 | 7 | using Microsoft.AspNetCore.Identity; |
6 | 8 | using Microsoft.AspNetCore.Mvc; |
7 | 9 | using Microsoft.AspNetCore.Mvc.RazorPages; |
| 10 | +using Microsoft.Extensions.Options; |
8 | 11 |
|
9 | 12 | namespace EssentialCSharp.Web.Areas.Identity.Pages.Account; |
10 | 13 |
|
11 | | -public class LoginModel(SignInManager<EssentialCSharpWebUser> signInManager, UserManager<EssentialCSharpWebUser> userManager, ILogger<LoginModel> logger, IReferralService referralService) : PageModel |
| 14 | +public class LoginModel(SignInManager<EssentialCSharpWebUser> signInManager, UserManager<EssentialCSharpWebUser> userManager, ILogger<LoginModel> logger, IReferralService referralService, ICaptchaService captchaService, IOptions<CaptchaOptions> optionsAccessor) : PageModel |
12 | 15 | { |
| 16 | + public CaptchaOptions CaptchaOptions { get; } = optionsAccessor.Value; |
13 | 17 | private InputModel? _Input; |
14 | 18 | [BindProperty] |
15 | 19 | public InputModel Input |
@@ -60,49 +64,119 @@ public async Task OnGetAsync(string? returnUrl = null) |
60 | 64 | public async Task<IActionResult> OnPostAsync(string? returnUrl = null) |
61 | 65 | { |
62 | 66 | returnUrl ??= Url.Content("~/"); |
| 67 | + string? hCaptcha_response = Request.Form[CaptchaOptions.HttpPostResponseKeyName]; |
63 | 68 |
|
64 | 69 | ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); |
65 | 70 |
|
66 | | - if (ModelState.IsValid) |
| 71 | + if (!ModelState.IsValid) |
67 | 72 | { |
68 | | - Microsoft.AspNetCore.Identity.SignInResult result; |
69 | | - if (Input.Email is null) |
70 | | - { |
71 | | - return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl }); |
72 | | - } |
73 | | - EssentialCSharpWebUser? foundUser = await userManager.FindByEmailAsync(Input.Email); |
74 | | - if (Input.Password is null) |
75 | | - { |
76 | | - return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl }); |
77 | | - } |
78 | | - if (foundUser is not null) |
79 | | - { |
80 | | - result = await signInManager.PasswordSignInAsync(foundUser, Input.Password, Input.RememberMe, lockoutOnFailure: true); |
81 | | - // Call the referral service to get the referral ID and set it onto the user claim |
82 | | - _ = await referralService.EnsureReferralIdAsync(foundUser); |
83 | | - } |
84 | | - else |
85 | | - { |
86 | | - result = await signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); |
87 | | - } |
88 | | - if (result.Succeeded) |
89 | | - { |
90 | | - logger.LogInformation("User logged in."); |
91 | | - return LocalRedirect(returnUrl); |
92 | | - } |
93 | | - if (result.RequiresTwoFactor) |
94 | | - { |
95 | | - return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); |
96 | | - } |
97 | | - if (result.IsLockedOut) |
| 73 | + return Page(); |
| 74 | + } |
| 75 | + |
| 76 | + if (hCaptcha_response is null) |
| 77 | + { |
| 78 | + ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, HCaptchaErrorDetails.GetValue(HCaptchaErrorDetails.MissingInputResponse).FriendlyDescription); |
| 79 | + return Page(); |
| 80 | + } |
| 81 | + |
| 82 | + HCaptchaResult? response = await captchaService.VerifyAsync(hCaptcha_response); |
| 83 | + if (response is null) |
| 84 | + { |
| 85 | + ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, "Error: HCaptcha API response unexpectedly null"); |
| 86 | + return Page(); |
| 87 | + } |
| 88 | + |
| 89 | + if (response.Success) |
| 90 | + { |
| 91 | + if (ModelState.IsValid) |
98 | 92 | { |
99 | | - logger.LogWarning("User account locked out."); |
100 | | - return RedirectToPage("./Lockout"); |
| 93 | + Microsoft.AspNetCore.Identity.SignInResult result; |
| 94 | + if (Input.Email is null) |
| 95 | + { |
| 96 | + return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl }); |
| 97 | + } |
| 98 | + EssentialCSharpWebUser? foundUser = await userManager.FindByEmailAsync(Input.Email); |
| 99 | + if (Input.Password is null) |
| 100 | + { |
| 101 | + return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl }); |
| 102 | + } |
| 103 | + if (foundUser is not null) |
| 104 | + { |
| 105 | + result = await signInManager.PasswordSignInAsync(foundUser, Input.Password, Input.RememberMe, lockoutOnFailure: true); |
| 106 | + // Call the referral service to get the referral ID and set it onto the user claim |
| 107 | + _ = await referralService.EnsureReferralIdAsync(foundUser); |
| 108 | + } |
| 109 | + else |
| 110 | + { |
| 111 | + result = await signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); |
| 112 | + } |
| 113 | + if (result.Succeeded) |
| 114 | + { |
| 115 | + logger.LogInformation("User logged in."); |
| 116 | + return LocalRedirect(returnUrl); |
| 117 | + } |
| 118 | + if (result.RequiresTwoFactor) |
| 119 | + { |
| 120 | + return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); |
| 121 | + } |
| 122 | + if (result.IsLockedOut) |
| 123 | + { |
| 124 | + logger.LogWarning("User account locked out."); |
| 125 | + return RedirectToPage("./Lockout"); |
| 126 | + } |
| 127 | + else |
| 128 | + { |
| 129 | + ModelState.AddModelError(string.Empty, "Invalid login attempt."); |
| 130 | + return Page(); |
| 131 | + } |
101 | 132 | } |
102 | | - else |
| 133 | + } |
| 134 | + else |
| 135 | + { |
| 136 | + switch (response.ErrorCodes?.Length) |
103 | 137 | { |
104 | | - ModelState.AddModelError(string.Empty, "Invalid login attempt."); |
105 | | - return Page(); |
| 138 | + case 0: |
| 139 | + throw new InvalidOperationException("The HCaptcha determined the passcode is not valid, and does not meet the security criteria"); |
| 140 | + case > 1: |
| 141 | + throw new InvalidOperationException("HCaptcha returned error codes: " + string.Join(", ", response.ErrorCodes)); |
| 142 | + default: |
| 143 | + { |
| 144 | + if (response.ErrorCodes is null) |
| 145 | + { |
| 146 | + throw new InvalidOperationException("HCaptcha returned error codes unexpectedly null"); |
| 147 | + } |
| 148 | + if (HCaptchaErrorDetails.TryGetValue(response.ErrorCodes.Single(), out HCaptchaErrorDetails? details)) |
| 149 | + { |
| 150 | + switch (details.ErrorCode) |
| 151 | + { |
| 152 | + case HCaptchaErrorDetails.MissingInputResponse: |
| 153 | + case HCaptchaErrorDetails.InvalidInputResponse: |
| 154 | + case HCaptchaErrorDetails.InvalidOrAlreadySeenResponse: |
| 155 | + ModelState.AddModelError(string.Empty, details.FriendlyDescription); |
| 156 | + logger.LogInformation("HCaptcha returned error code: {ErrorDetails}", details.ToString()); |
| 157 | + break; |
| 158 | + case HCaptchaErrorDetails.BadRequest: |
| 159 | + ModelState.AddModelError(string.Empty, details.FriendlyDescription); |
| 160 | + logger.LogInformation("HCaptcha returned error code: {ErrorDetails}", details.ToString()); |
| 161 | + break; |
| 162 | + case HCaptchaErrorDetails.MissingInputSecret: |
| 163 | + case HCaptchaErrorDetails.InvalidInputSecret: |
| 164 | + case HCaptchaErrorDetails.NotUsingDummyPasscode: |
| 165 | + case HCaptchaErrorDetails.SitekeySecretMismatch: |
| 166 | + logger.LogCritical("HCaptcha returned error code: {ErrorDetails}", details.ToString()); |
| 167 | + break; |
| 168 | + default: |
| 169 | + throw new InvalidOperationException("HCaptcha returned unknown error code: " + details?.ErrorCode); |
| 170 | + } |
| 171 | + } |
| 172 | + else |
| 173 | + { |
| 174 | + throw new InvalidOperationException("HCaptcha returned unknown error code: " + response.ErrorCodes.Single()); |
| 175 | + } |
| 176 | + |
| 177 | + break; |
| 178 | + } |
| 179 | + |
106 | 180 | } |
107 | 181 | } |
108 | 182 |
|
|
0 commit comments