Skip to content

Share captcha validation policy#1121

Open
BenjaminMichaelis wants to merge 3 commits into
mainfrom
benjaminmichaelis/super-giggle
Open

Share captcha validation policy#1121
BenjaminMichaelis wants to merge 3 commits into
mainfrom
benjaminmichaelis/super-giggle

Conversation

@BenjaminMichaelis
Copy link
Copy Markdown
Member

@BenjaminMichaelis BenjaminMichaelis commented May 17, 2026

This change removes the per-page captcha handling drift and centralizes the policy in one shared validation service.

What changed

  • Added ICaptchaValidationService with explicit outcomes for missing token, unavailable, invalid, and valid captcha states.
  • Switched chat and the identity pages to the shared policy layer.
  • Kept register's detailed hCaptcha error-code mapping intact while preserving the simpler generic messages elsewhere.
  • Added tests for the shared validation behavior.
  • Missing captcha configuration now fails closed instead of allowing requests through.

Notes

  • ICaptchaService still owns raw hCaptcha verification.
  • Chat keeps its existing 403 vs 503 behavior for invalid vs unavailable captcha.

Copilot AI review requested due to automatic review settings May 17, 2026 07:19
{
if (HCaptchaErrorDetails.TryGetValue(response.ErrorCodes.Single(), out HCaptchaErrorDetails? details))
{
switch (details.ErrorCode)
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Consolidates per-page hCaptcha handling into a single ICaptchaValidationService that returns explicit outcomes (Disabled / MissingToken / Unavailable / Invalid / Valid), and updates chat plus the Identity pages (Login, Register, ForgotPassword, ResetPassword, ResendEmailConfirmation) to use it. Register retains its detailed hCaptcha error-code mapping; other callers use a generic message. Adds TUnit tests for the new service.

Changes:

  • New shared service CaptchaValidationService, result record, and outcome enum centralizing captcha policy.
  • Identity pages and ChatController migrated off direct ICaptchaService usage to the shared validation layer.
  • Added CaptchaValidationServiceTests covering disabled, missing token, unavailable, invalid, and valid paths.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
EssentialCSharp.Web/Services/ICaptchaValidationService.cs New interface with two ValidateAsync overloads.
EssentialCSharp.Web/Services/CaptchaValidationService.cs Implements the shared policy: disabled / missing / unavailable / invalid / valid.
EssentialCSharp.Web/Services/CaptchaValidationResult.cs Result record exposing ShouldProceed.
EssentialCSharp.Web/Services/CaptchaValidationOutcome.cs Enum of validation outcomes.
EssentialCSharp.Web/Extensions/IServiceCollectionExtensions.cs Registers ICaptchaValidationService as singleton.
EssentialCSharp.Web/Controllers/ChatController.cs Replaces inline captcha logic with shared service; preserves 503/403 responses and logging.
EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs Switches to shared validator; keeps generic failure message.
EssentialCSharp.Web/Areas/Identity/Pages/Account/Register.cshtml.cs Restructures captcha branch to use outcome-based switch while retaining detailed error mapping.
EssentialCSharp.Web/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs Uses shared validator with generic failure message.
EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs Uses shared validator with generic failure message.
EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs Uses shared validator with generic failure message.
EssentialCSharp.Web.Tests/CaptchaValidationServiceTests.cs TUnit tests for all five outcomes via a stub captcha service.

Comment on lines +109 to +111
public Task<HCaptchaResult?> VerifyAsync(string secret, string response, string sitekey, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();

}

if (string.IsNullOrEmpty(hCaptcha_response))
CaptchaValidationResult captchaResult = await captchaValidationService.ValidateAsync(hCaptcha_response, HttpContext.Connection.RemoteIpAddress?.ToString());
Comment on lines +15 to +17
if (string.IsNullOrWhiteSpace(Options.SecretKey) || string.IsNullOrWhiteSpace(Options.SiteKey))
return new CaptchaValidationResult(CaptchaValidationOutcome.Disabled, null);

Copilot AI review requested due to automatic review settings May 18, 2026 05:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs:75

  • This change quietly alters behavior for the identity pages when captcha configuration is missing. Previously, CaptchaService.VerifyAsync threw InvalidOperationException if SecretKey/SiteKey were null (CaptchaService.cs:36-37), which surfaced misconfiguration loudly. Now the Disabled outcome falls through this check and the user simply sees "Human verification failed. Please try again." with no log entry, making misconfiguration silently break sign-in/forgot-password/etc. Consider either explicitly handling CaptchaValidationOutcome.Disabled with a dedicated log (similar to LogCaptchaConfigurationMissing in ChatController) or with a distinct user-facing message so this state is observable. The same concern applies to ForgotPassword.cshtml.cs, ResendEmailConfirmation.cshtml.cs, and ResetPassword.cshtml.cs.
        CaptchaValidationResult captchaResult = await captchaValidationService.ValidateAsync(captchaToken, HttpContext.Connection.RemoteIpAddress?.ToString());
        if (!captchaResult.ShouldProceed)
        {
            ModelState.AddModelError(string.Empty, "Human verification failed. Please try again.");
            ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            return Page();

Comment on lines +110 to 115
if (response is null)
{
ModelState.AddModelError(string.Empty, "Error: Email may not be null.");
LogHCaptchaNullErrorCodes(logger);
ModelState.AddModelError(string.Empty, "Captcha verification failed. Please try again.");
return Page();
}
Comment on lines +49 to 66
if (captchaValidation.Outcome == CaptchaValidationOutcome.Disabled)
{
LogCaptchaConfigurationMissing(_Logger);
return StatusCode(503, new { error = "Human verification is temporarily unavailable. Please try again later.", errorCode = "captcha_unavailable" });
}

if (captchaValidation.Outcome == CaptchaValidationOutcome.Unavailable)
{
LogCaptchaServiceUnavailable(_Logger);
return StatusCode(503, new { error = "Human verification is temporarily unavailable. Please try again later.", errorCode = "captcha_unavailable" });
}

if (captchaValidation.Outcome == CaptchaValidationOutcome.Invalid)
{
LogCaptchaValidationFailed(_Logger, string.Join(',', captchaValidation.Response?.ErrorCodes ?? []));
}

return StatusCode(403, new { error = "Human verification required.", errorCode = "captcha_failed" });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants