Google Invisible reCAPTCHA and reCAPTCHA v2 in ASP.NET Core

ASP.NET Core implementation of Google reCAPTCHA and reCAPTCHA v2

Recently I wrote and article about setting up and using Google reCAPTCHA v2, better known as "I'm not a robot" validation. If you were setting up reCAPTCHA v2, you probably noticed that there is an option for "Invisible reCAPTCHA" option.

This type of validation, instead of asking user to interact with the UI in order to be identified as a human, does not require any direct interaction which can help building better user experience for your visitors. This might be the reason why many websites decide to use Invisible over reCAPTCHA v2 on their website for filtering bot traffic.

Technically, from the implementation perspective, it is not much different from reCAPTCHA v2, so I decided to extend my GitHub project to support both Invisible and v2 types of reCAPTCHA.

In this article I will mainly focus on Invisible reCAPTCHA implementation. If you want to know more how Google reCATCHA works in general or you want to focus to only reCAPtCHA v2 I recomend you go through Google Invisible reCAPTCHA and reCAPTCHA v2 in ASP.NET Core Web Applications article first.

How to setup Invisible reCAPTCHA

First thing in setting up Gogole Invisible reCATCHA is to generate client and server keys from Google reCAPTCHA admin page https://www.google.com/recaptcha/admin.

Invisible Recpatcha

Make sure you select Invisible reCAPTCHA option and enter domains of your website. For testing from your local machine, localhost will do the trick.

Note

In case you already have your website registered for reCAPTCHA v2, you cannot re-use the same client and server keys for Invisible reCAPTCHA as they are reCAPTCHA type specific. You have to create separate entries for both reCAPTCHA types in your reCAPTCHA admin page

After clicking register you will get you set of keys which consists of client and server key.

Invisible Recpatcha Keys

ASP.NET Core implementation

As I mentioned in the beginning of this article, both reCAPTCHA v2 and Invisible reCAPTCHA are pretty much the same from the point of implementation on the client and server side, so I will briefly explain how both of them are implemented with a bit more focus on Invisible reCAPTCHA option. You will see that there is a lot of common code used for both options as they are pretty similar in terms of implementation from Google side.

First this we need to to is definately to supply the application with client snd server keys. We'll do that using JSON configuration and then inject configuration through IConfiguration interface.

{
  "GoogleReCaptcha": {
    "ClientKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "SecretKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
}
    

Once we have our keys in config file, we need to inject the configuration in Startup.cs

using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Sample.Core.GoogleReCaptcha.Web
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940

        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration, IHostingEnvironment env)
        {
            Configuration = configuration;
         
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IConfiguration>(new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile($"appsettings.json")
                .Build());

            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

    

With reCAPTCHA we are basically validating our model we are going to submit with the html form, so it makes sense to implement this as custom ASP.NET core validation using custom validation attribute.

using Microsoft.Extensions.Configuration;
using Newtonsoft.Json.Linq;
using System;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Net.Http;

namespace Sample.Core.GoogleReCaptcha.Web.Validation
{

    public class GoogleReCaptchaValidationAttribute : ValidationAttribute
    {
       
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            Lazy<ValidationResult> errorResult = new Lazy<ValidationResult>(() => new ValidationResult("Google reCAPTCHA validation failed", new String[] { validationContext.MemberName }));

            if (value == null || String.IsNullOrWhiteSpace( value.ToString())) 
            {
                return errorResult.Value;
            }

            IConfiguration configuration = (IConfiguration)validationContext.GetService(typeof(IConfiguration));
            String reCaptchResponse = value.ToString();
            String reCaptchaSecret = configuration.GetValue<String>("GoogleReCaptcha:SecretKey");
            

            HttpClient httpClient = new HttpClient();
            var httpResponse = httpClient.GetAsync($"https://www.google.com/recaptcha/api/siteverify?secret={reCaptchaSecret}&response={reCaptchResponse}").Result;
            if (httpResponse.StatusCode != HttpStatusCode.OK)
            {
                return errorResult.Value;
            }

            String jsonResponse = httpResponse.Content.ReadAsStringAsync().Result;
            dynamic jsonData = JObject.Parse(jsonResponse);
            if (jsonData.success != true.ToString().ToLower())
            {
                return errorResult.Value;
            }

            return ValidationResult.Success;

        }
    }
}

    

This custom validation attribute can be used by multiple models, so to make it easy I decided to use an abstract class model.

using Microsoft.AspNetCore.Mvc;
using System;
using System.ComponentModel.DataAnnotations;

namespace Sample.Core.GoogleReCaptcha.Web.Validation
{
    public abstract class GoogleReCaptchaModelBase
    {
        [Required]
        [GoogleReCaptchaValidation]
        [BindProperty(Name = "g-recaptcha-response")]
        public String GoogleReCaptchaResponse { get; set; }
    }
}

    

Now we can inherit our abstract class to any model we want to use. For the sample purpose I picked a simple demo model for comment whic hase cmment title and comment body text

using System;
using System.ComponentModel.DataAnnotations;
using Sample.Core.GoogleReCaptcha.Web.Validation;
using Newtonsoft.Json;

namespace Sample.Core.GoogleReCaptcha.Web.Models
{
    public class Comment : GoogleReCaptchaModelBase
    {
        [Required]
        public String Title { get; set; }

        [Required]
        public String Content { get; set; }
    }
}

    

Our sample model is now ready for validation, so we can focus on out UI representation. One of the easiest and reusable ways of using html components is HtmlHelper. Since both Invisible reCAPTCHA and reCAPTCHA v2 have pretty much similar structure we can use common class for rendering both types

using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.IO;

namespace Sample.Core.GoogleReCaptcha.Web.Validation
{
    public static class GoogleReCaptchaTagHelper
    {
        public static IHtmlContent GoogleReCaptcha(this IHtmlHelper htmlHelper, String siteKey, String callback = null)
        {
            var tagBuilder = GetReCaptchaTag("div", siteKey, callback);
            return GetHtmlContent(htmlHelper, tagBuilder);
        }


        public static IHtmlContent GoogleInvisibleReCaptcha(this IHtmlHelper htmlHelper, String text,  String siteKey, String callback=null)
        {
            var tagBuilder = GetReCaptchaTag("button", siteKey, callback);
            tagBuilder.InnerHtml.Append(text);
            return GetHtmlContent(htmlHelper, tagBuilder);
        }

        private static TagBuilder GetReCaptchaTag(String tagName, String siteKey, String callback = null)
        {
            var tagBuilder = new TagBuilder(tagName);
            tagBuilder.Attributes.Add("class", "g-recaptcha");
            tagBuilder.Attributes.Add("data-sitekey", siteKey);
            if (callback != null && !String.IsNullOrWhiteSpace(callback))
            {
                tagBuilder.Attributes.Add("data-callback", callback);
            }

            return tagBuilder;
        }

        private static IHtmlContent GetHtmlContent(IHtmlHelper htmlHelper,TagBuilder tagBuilder)
        {
            using (var writer = new StringWriter())
            {
                tagBuilder.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default);
                var htmlOutput = writer.ToString();
                return htmlHelper.Raw(htmlOutput);
            }
        }


    }
}

    

Now that we have both HtmlHelper implementations we can render it out in the razor, but since we need separate keys we will need separate views, so here they go

reCAPTCHA v2 Razor View

    @using (Html.BeginForm(method: FormMethod.Post, controllerName: "Comments", actionName: "NoRobot", htmlAttributes: new { id = "comment-form" }))
    {
        <div>
            <label for="Title">Title</label>
            <input type="text" name="Title" id="Title" />
        </div>
        <div>
            <label for="Content">Content</label>
            <textarea type="text" name="Content" id="Content" rows="8" cols="20"></textarea>
        </div>
        <div>
            @Html.GoogleReCaptcha(Configuration.GetValue<String>("GoogleReCaptcha:ClientKey"))
        </div>
        <div>
            <button type="submit">Send comment</button>
        </div>
        @if (!ViewData.ModelState.IsValid)
        {
            <ul>
                @foreach (var modelState in ViewData.ModelState.Values)
                {
                    foreach (var error in modelState.Errors)
                    {
                        <li>@error.ErrorMessage</li>
                    }
                }
            </ul>
        }
    }
    

Invisible reCAPTCHA Razor View

    @using (Html.BeginForm(method: FormMethod.Post, controllerName: "Comments", actionName: "Invisible", htmlAttributes: new { id = "comment-form" }))
    {
        <div>
            <label for="Title">Title</label>
            <input type="text" name="Title" id="Title" />
        </div>
        <div>
            <label for="Content">Content</label>
            <textarea type="text" name="Content" id="Content" rows="8" cols="20"></textarea>
        </div>
        <div>
            @Html.GoogleInvisibleReCaptcha("Submit form",Configuration.GetValue<String>("GoogleReCaptcha:ClientKey"), "submitForm")
        </div>
        @if (!ViewData.ModelState.IsValid)
        {
            <ul>
                @foreach (var modelState in ViewData.ModelState.Values)
                {
                    foreach (var error in modelState.Errors)
                    {
                        <li>@error.ErrorMessage</li>
                    }
                }
            </ul>
        }
    }
    

Since button tag used for Invisible reCAPTCHA cannot perform submit we are using a callback JavaScript function to do this

        function submitForm() {
            document.forms[0].submit();
        }
    

Now to test this we need two routings to these separate Razor Views which can be simply done by a controller with two GET and POST actions for initial rendering and post-submit rendering of the view. 

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article