How to use hCaptcha in .Net Core

I have been using reCaptcha on few of my websites for few years now. It has been effective. Recently I came across hCaptcha. I liked how it verifies that a person that is verifying captcha is a human and not a robot. So I decided to incorporate hCaptcha in my .Net Core based web application. In this blog I will talk about all the steps that you need to complete to use hCaptcha in a .Net Core application.

Create hCaptcha account

First step in this process is going to be creation of an account at hCaptcha. If you already have an account you can skip to next step.

Add Website to hCaptcha

You will need to add an entry of your website in hCaptcha where you want to use it. Go to Site List page and click on New Site button.

Enter following pieces of required information.

  • Host Names: This is domain of your website. E.g. worldofhobbycraft.com is entry that I use for my website.
  • Difficulty: You can pick how hard you want to make it a for a user. Keep in mind that if you make it too difficult, then an actual user may also get turned off. So choose this option based on your business requirement.
  • Audience Interests: This list will decide the type of images a user will be shown.

Click on Save to complete the process.

Public & Private Key

After a website entry has been added, you will be issued a public key. You will use this public key on your web pages where hCaptcha will be shown to users. A private key is assigned to each account. It is an account wide value that is used for all the web sites that you have added. You will find this value under your account settings.

Add HTML Elements

Following are bare minimum elements that you will need on your page.

<div class="h-captcha" data-sitekey="{YOUR PUBLIC KEY}"></div>
<script src="https://hcaptcha.com/1/api.js" async defer></script>

You will add above elements inside your FORM element that is going to POST values to server where you are going to verify captcha. Following is a code snippet that I added on my test web page.

<form asp-controller="Security" asp-action="TestHCaptcha" method="post">
    <input asp-for="Test"/>
    <amgr-captcha />
    <button type="submit">Test It</button>
</form>

You are wondering where are the HTML elements that I mentioned that you need to make hCaptcha work on your page. I have adopted lot of implementation from nopCommerce platform. I reused the TagHelper that was implemented in that framework. Luckily hCaptcha is mostly compatible with reCaptcha. I am able to reuse same TagHelper. All I had to do is add new properties to store hCaptch specific public & private keys and URLs. Following are code snippets from the code that I used.

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (context == null)
                throw new ArgumentNullException(nameof(context));

            if (output == null)
                throw new ArgumentNullException(nameof(output));

            //contextualize IHtmlHelper
            var viewContextAware = _htmlHelper as IViewContextAware;
            viewContextAware?.Contextualize(ViewContext);

            IHtmlContent captchaHtmlContent;
            switch (_captchaSettings.CaptchaType)
            {
                case CaptchaType.CheckBoxReCaptchaV2:
                    output.Attributes.Add("class", "captcha-box");
                    captchaHtmlContent = await _htmlHelper.GenerateCheckBoxReCaptchaV2Async(_captchaSettings);
                    break;
                case CaptchaType.ReCaptchaV3:
                    captchaHtmlContent = await _htmlHelper.GenerateReCaptchaV3Async(_captchaSettings);
                    break;
                case CaptchaType.HCaptcha:
                    output.Attributes.Add("class", "h-captcha");
                    output.Attributes.Add("data-sitekey", _captchaSettings.HCaptchaPublicKey);
                    captchaHtmlContent = await _htmlHelper.GenerateHCaptchaAsync(_captchaSettings);
                    break;
                default:
                    throw new InvalidOperationException("Invalid captcha type.");
            }

            //tag details
            output.TagName = "div";
            output.TagMode = TagMode.StartTagAndEndTag;
            output.Content.SetHtmlContent(captchaHtmlContent);
        }

public static async Task GenerateHCaptchaAsync(this IHtmlHelper helper,
            CaptchaSettings captchaSettings)
        {
            // Lot of hCaptcha features are compatible with reCaptcha. It has same
            // list of supported languages as reCaptcha. So we are going to just
            // use the function for reCaptcha to get language.
            var language = await GetReCaptchaLanguageAsync(captchaSettings);

            //prepare identifier
            var id = $"captcha_{CommonHelper.GenerateRandomInteger()}";

            //prepare public key
            var publicKey = captchaSettings.HCaptchaPublicKey ?? string.Empty;
            //prepare theme
            var theme = (captchaSettings.ReCaptchaTheme ?? string.Empty).ToLower();
            theme = theme switch
            {
                "blackglass" or "dark" => "dark",
                "clean" or "red" or "white" or "light" => "light",
                _ => "light",
            };

            //generate hCAPTCHA Control
            var scriptCallbackTag = new TagBuilder("script") { TagRenderMode = TagRenderMode.Normal };
            scriptCallbackTag.InnerHtml
                .AppendHtml($"var onloadCallback{id} = function() {{hcaptcha.render('{id}',
                       {{'sitekey' : '{publicKey}', 'theme' : '{theme}' }});}};");
            
            var captchaTag = new TagBuilder("div") { TagRenderMode = TagRenderMode.Normal };
            captchaTag.Attributes.Add("id", id);

            var scriptLoadApiTag = GenerateLoadApiScriptTag(captchaSettings, id, "explicit", language);

            return new HtmlString(await scriptCallbackTag.RenderHtmlContentAsync() +
                  await captchaTag.RenderHtmlContentAsync() + await scriptLoadApiTag.RenderHtmlContentAsync());
        }

Now you should have hCaptcha showing on your web page.

Validate Captcha On Server Side

Last step in the process is validating user's captcha response on server side. I have a CaptchaValidateAttribute filter on POST method to validate user's response.

        [HttpPost]
        [ValidateCaptcha]
        public async Task<IActionResult> TestHCaptcha(SecurityTestModel model)
        {
            // Print the captcha response values for debugging only!
            Debug.WriteLine($"g-recaptcha-response: {Request.Form["g-recaptcha-response"]}");
            Debug.WriteLine($"h-captcha-response: {Request.Form["h-captcha-response"]}");

            return View();
        }

After a user has solved the question puzzle and submitted values to server as part of POST request, hCaptcha will insert response value in h-captcha-response FORM field. You will need to verify this value with hCaptcha site. This is where private key of your hCaptcha will come into play. Following code from the filter shows how this validation takes place.

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
            {
                await ValidateCaptchaAsync(context);
                if (context.Result == null)
                    await next();
            }

private async Task ValidateCaptchaAsync(ActionExecutingContext context)
            {
                if (context == null)
                    throw new ArgumentNullException(nameof(context));

                if (!await DataSettingsManager.IsDatabaseInstalledAsync())
                    return;

                //whether CAPTCHA is enabled
                if (_captchaSettings.Enabled && context.HttpContext?.Request != null)
                {
                    //push the validation result as an action parameter
                    var isValid = false;

                    //get form values
                    var captchaResponseValue = context.HttpContext.Request.Form[RESPONSE_FIELD_KEY];
                    var gCaptchaResponseValue = context.HttpContext.Request.Form[G_RESPONSE_FIELD_KEY];
                    var hCaptchaResponseValue = context.HttpContext.Request.Form[H_RESPONSE_FIELD_KEY];

                    if (!StringValues.IsNullOrEmpty(captchaResponseValue) ||
                        !StringValues.IsNullOrEmpty(gCaptchaResponseValue) ||
                        !StringValues.IsNullOrEmpty(hCaptchaResponseValue))
                    {
                        //validate request
                        try
                        {
                            var value = _captchaSettings.CaptchaType switch
                            {
                                CaptchaType.HCaptcha => hCaptchaResponseValue,
                                _ => !StringValues.IsNullOrEmpty(captchaResponseValue)
                                    ? captchaResponseValue
                                    : gCaptchaResponseValue
                            };
                            var response = await _captchaHttpClient.ValidateCaptchaAsync(value);

                            switch (_captchaSettings.CaptchaType)
                            {
                                case CaptchaType.CheckBoxReCaptchaV2:
                                    isValid = response.IsValid;
                                    break;

                                case CaptchaType.ReCaptchaV3:
                                    isValid = response.IsValid &&
                                        response.Action == context.RouteData.Values["action"].ToString() &&
                                        response.Score > _captchaSettings.ReCaptchaV3ScoreThreshold;
                                    break;
                                case CaptchaType.HCaptcha:
                                    isValid = response.IsValid;
                                    break;
                                default:
                                    break;
                            }
                        }
                        catch (Exception exception)
                        {
                            await _logger.ErrorAsync("Error occurred on CAPTCHA validation", exception, 
                            await _workContext.GetCurrentUserAsync());
                        }
                    }

                    context.ActionArguments[_actionParameterName] = isValid;
                }
                else
                    context.ActionArguments[_actionParameterName] = false;
            }

public virtual async Task<CaptchaResponse> ValidateCaptchaAsync(string responseValue)
        {
            //get response
            var response = _captchaSettings.CaptchaType switch
            {
                CaptchaType.HCaptcha => await ValidateHCaptchaAsync(responseValue),
                _ => await ValidateReCaptchaAsync(responseValue)
            };
            return JsonConvert.DeserializeObject&CaptchaResponse>(response);

        }

private async Task<string> ValidateHCaptchaAsync(string responseValue)
        {
            // Prepare POST request data.
            var content = new FormUrlEncodedContent(new Dictionary<string, string>
            {
                ["secret"] = _captchaSettings.HCaptchaPrivateKey,
                ["response"] = responseValue,
                ["remoteip"] = _webHelper.GetCurrentIpAddress()
            });
            var responseMessage = await _httpClient.PostAsync(AmgrSecurityDefaults.HCaptchaValidationPath, content);
            var response = await responseMessage.Content.ReadAsStringAsync();
            return response;
        }

public partial class CaptchaResponse
    {
         public CaptchaResponse()
        {
            Errors = new List<string>();
        }

        [JsonProperty(PropertyName = "action")]
        public string Action { get; set; }

        [JsonProperty(PropertyName = "score")]
        public decimal Score { get; set; }

        [JsonProperty(PropertyName = "success")]
        public bool IsValid { get; set; }

        [JsonProperty(PropertyName = "challenge_ts")]
        public DateTime? ChallengeDateTime { get; set; }

        [JsonProperty(PropertyName = "hostname")]
        public string Hostname { get; set; }

        [JsonProperty(PropertyName = "error-codes")]
        public List<string> Errors { get; set; }

        [JsonProperty("score_reason")]
        public List<string> ScoreReasons { get; set; }
    }

This is all that I did to incorporate hCaptcha in my web site. As you can see that I was able to reuse most of the code from nopCommerce platform that I use for my website's framework.

If you have any questions, let me know.

Search

Social

Weather

18.5 °C / 65.2 °F

weather conditions Clouds

Monthly Posts

Blog Tags