hcaptcha is a popular captcha service that is used to verify that a user is a human rather than a bot. Captcha, which stands for "Completely Automated Public Turing test to tell Computers and Humans Apart", is a test that is designed to be easy for humans to solve but difficult for machines. Captcha services are used to protect websites from automated spam and abuse. While there are many captcha services available, hcaptcha is becoming increasingly popular due to its superior security and usability compared to other captcha services. This tutorial describes how you can employ hcaptcha cleanly in your Asp.Net Core razor pages with page filters. In the next tutorial, you learn how to use hcaptcha in your Asp.Net Core MVC controllers with action filters. Before delving into that, it would be a good idea to know how hcaptcha works.

In simple terms, when the user clicks on the "I am Human" button, he is shown a puzzle. After the successful solving of the puzzle, a Token is generated for him. The app's client-side posts its string SiteKey, which is the app's public key, along with the Token, which is created based on the SiteKey, to the app's server. The server that has a string Secret, which is the app's private key, sends the Token and Secret to the hcaptcha's server through an HTTP call. At this point, the server validates the inputs and determines whether the Token and Secret are valid. If not, the server provides your app with the failure reason.

1- Create an account on hCaptcha website

As the title suggests, do so by filling out the form and receiving your dedicate SiteKey and Secret. In your production environment, you should use the provided SiteKey and Secret.

2-Scaffold identity pages

Asp.Net Core projects by default do not explicitly have Razor identity pages. When the user makes a request to either of them, they become rendered. But, if you want to customize these pages, you need to scaffold these pages. As we want to add the hcaptcha to the login page, we just add the login page. To do so, right-click on the Identity folder in the Area folder, and then from Add menu, click New Scaffold Item. On the opened window, from the left side, click Identity and then select your desired pages to add. 

3-Add HCaptcha server-side files

I have divided the primary functionalities into three classes, namely HCaptchaVerifier, HCaptchaConfigurationProvider, and HCaptchaResult.

HCaptchaVerifier is responsible for validating the token and Secret by posting them to the hcaptcha’s server. HCaptchaConfigurationProvider is responsible for providing your app's client-side and server-side with SiteKey and Secret. Finally, HCaptchaResult collects the return data coming from the hcaptcha’s server. By inspecting the data from the object of this class, we can say whether the secret and token were valid.

public class HCaptchaVerifier
{
  private readonly IHttpClientFactory _clientFactory;
  private readonly HCaptchaConfigurationProvider _hCaptchaConfigurationProvider;

  public HCaptchaVerifier(IHttpClientFactory clientFactory,
    HCaptchaConfigurationProvider hCaptchaConfigurationProvider)
  {
    _clientFactory = clientFactory;
    _hCaptchaConfigurationProvider = hCaptchaConfigurationProvider;
  }

  public async Task<bool> Verify(string responseToken)
  {
    if (_hCaptchaConfigurationProvider.IsDevelopment)
    {
      return true;
    }

    if (string.IsNullOrWhiteSpace(responseToken))
    {
      return false;
    }

    var hCaptchaResult = await MakeApiCallToHCaptchaServer(
        _hCaptchaConfigurationProvider.GetSecret(), responseToken);

    return hCaptchaResult.Success;
  }
  private async Task<HCaptchaResult> MakeApiCallToHCaptchaServer
    (string secret, string responseToken)
  {
    var client = _clientFactory.CreateClient("hCaptcha");

    var postData = new List<KeyValuePair<string, string>>
    {
      new KeyValuePair<string, string>("secret", secret),
      new KeyValuePair<string, string>("response", responseToken),
    };

    var httpResponseMessage = await client.PostAsync(
      "/siteverify", new FormUrlEncodedContent(postData));

    return await httpResponseMessage
      .Content.ReadFromJsonAsync<HCaptchaResult>();
  }
}

4-Create page filters

To employ the mentioned provider and verifier in a loosely coupled manner for our hcaptcha, we have created two page filters, namely ConfigureHCaptchaPageFilter and ProtectByHCaptchaPageFilter. We just annotate the login page, attached to “Login.cshtml,” with these two page filters. The former page filter receives the SiteKey and adds it to a ViewData. Later in our client-side code, we will use SiteKey for initializing the hcaptcha component. The latter page filter uses the verifier to verify the received token. If the verification fails, it returns a bad request response. Needless to say, the token is automatically sent to the server through form data.

[AttributeUsage(AttributeTargets.Class)]
public class ConfigureHCaptchaPageFilter : Attribute, IAsyncPageFilter
{
  public Task OnPageHandlerSelectionAsync
    (PageHandlerSelectedContext context)
  {
    return Task.CompletedTask;
  }

  public async Task OnPageHandlerExecutionAsync
    (PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
  {
    await next();

    if (context.HandlerInstance is PageModel pageModel)
    {
      var hCaptchaConfigurationProvider = context.HttpContext
        .RequestServices.GetService<HCaptchaConfigurationProvider>();

      pageModel.ViewData["hCaptchaSitekey"] =
        hCaptchaConfigurationProvider.GetSiteKey();
    }
  }
}
[AttributeUsage(AttributeTargets.Class)]
public class ProtectByHCaptchaPageFilter : Attribute, IAsyncPageFilter
{
  public Task OnPageHandlerSelectionAsync
    (PageHandlerSelectedContext context)
  {
    return Task.CompletedTask;
  }

  public async Task OnPageHandlerExecutionAsync
    (PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
  {
    if (context.HandlerMethod.MethodInfo.Name == "OnPostAsync")
    {
      var hCaptchaVerifier = context.HttpContext
        .RequestServices.GetService<HCaptchaVerifier>();

      string hCaptchaToken =
        context.HttpContext.Request.Form["h-captcha-response"];

      var verificationResult =
        await hCaptchaVerifier.Verify(hCaptchaToken);
      if (!verificationResult)
      {
        context.Result =
          new BadRequestObjectResult("hCaptcha Token is invalid!");
        return;
      }
    }

    await next();
  }
}
[ConfigureHCaptchaPageFilter]
[ProtectByHCaptchaPageFilter]
[AutoValidateAntiforgeryToken]
public class LoginModel : PageModel
{
    public async Task OnGetAsync(string returnUrl = null)
    {
        .
        .
    }


    public async Task<IActionResult> OnPostAsync(string returnUrl = null)
    {
        .
        .
    }
}

5-Add HCaptcha DI configuration file

In the next step, we should register our verifier and provider. I have extracted the registration code into a separate extension method file. This way, our codes would be much cleaner, and the registration codes do not clutter the "program.cs" file.

public static class HCaptchaConfigurations
{
  public static WebApplicationBuilder ConfigureHCaptcha
    (this WebApplicationBuilder builder)
  {
    builder.Services.AddHttpClient("hCaptcha", httpClient =>
    {
      httpClient.BaseAddress = new Uri("https://hcaptcha.com/");
    });

    builder.Services.AddSingleton<HCaptchaVerifier>();
    builder.Services.AddSingleton<HCaptchaConfigurationProvider>();

    return builder;
  }
}

6-Add HCaptcha DI configuration file to program.cs

Here, we should add our extension method to the WebApplicationBuilder pipeline.

public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
.
.
builder.ConfigureHCaptcha();
.
.
var app = builder.Build();
.
.
}

7-Add appsettings entry

To help our provider find SiteKey and Secrect and feed the dependent classes with this data, we add our SiteKey and Secret to the "appsettings.json" file. Note that the JSON property named IsDevelopment determines whether we work in development enviroment or not. For your production appsettings.json file, simply ignore this field. Also, the default values you see in the appsettings.json for SiteKey and Secret have been defined by the hcaptcha team for testing purposes. If you provide the mentioned values, the hcaptcha server always returns true after the token verification process.

{
.
.,
 "HCaptcha": {
  "SiteKey": "20000000-ffff-ffff-ffff-000000000002",
  "Secret": "0x0000000000000000000000000000000000000000",
  "DevelopmentMode": true
 },
.
.
}

8-Add HCaptcha client-side files

I have placed the JavaScript code in a file named h-captcha.js. In this file, we constantly check whether the captcha has been solved by inspecting an HTML element. If it becomes solved, it is filled with a value causing the submit button becomes enabled. Additionally, this file is also in charge of appending the hcaptcha element to a wrapper div supposed to show the hcaptcha.

(function () {
  setInterval(hCaptchaChangeDetectorCallback, 1000);
})();
function initializeHCaptcha(sitekey) {  
  $('#hcaptcha-wrapper')
    .append(`<div class="h-captcha" data-sitekey="${sitekey}"></div>`);     
}
function hCaptchaChangeDetectorCallback() {
  const hCaptchaValue = $('[name=h-captcha-response]').val();
  if (hCaptchaValue === "" || hCaptchaValue === undefined) {     
    $('#submit-button').prop('disabled', true);
  }
  else {
    $('#submit-button').prop('disabled', false);
  }
}

9-Add hcaptcah to login page 

Open the scaffolded "Login.cshtml" file. Firstly, create a hcaptcha wrapper div with id equal to hcaptcha-wrapper.Then, assign the id submit-button to the submit button. We disable the button by default to prevent the user from submitting the form without the token. Finally, create a script section at the end of the file and put the following codes in it. The following code adds the official hcaptcha JavaScript file to our code. We also pass the SiteKey's value to the hcaptcha initializer method via ViewData.

<form id="account" method="post">
    .
    .
    <h2>Use a local account to log in.</h2>
    <hr />
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <div class="form-floating">
        <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
        <label asp-for="Input.Email" class="form-label"></label>
        <span asp-validation-for="Input.Email" class="text-danger"></span>
    </div>
    <div class="form-floating">
        <input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" />
        <label asp-for="Input.Password" class="form-label"></label>
        <span asp-validation-for="Input.Password" class="text-danger"></span>
    </div>
    <div>
        <div class="checkbox">
            <label asp-for="Input.RememberMe" class="form-label">
                <input class="form-check-input" asp-for="Input.RememberMe" />
                @Html.DisplayNameFor(m => m.Input.RememberMe)
            </label>
        </div>
    </div>
    <div>
        <button id="submit-button" type="submit" class="w-100 btn btn-lg btn-primary" disabled>Log in</button>
    </div>
    .
    .
    <div id="hcaptcha-wrapper"></div>
</form>
.
.
@section Scripts {
  <partial name="_ValidationScriptsPartial" />
  <script src="https://js.hcaptcha.com/1/api.js" async defer></script>
  <script src="~/js/h-captcha.js"></script>
  <script>
    initializeHCaptcha('@ViewData["hCaptchaSitekey"]');
  </script>
}