
How to implement Google reCAPTCHA v3 in a Umbraco v9
By: Steve Adams,
Any site that contains forms will undoubtably suffer from spam at some point.
In the past, I've used a honey pot technique in an attempt to thwart the spammers. This involved adding a hidden field in the form. A real user would not see the hidden field and so when the form was submitted, the hidden field would have a blank value. Spam bots never used to check the hidden field status and would populate the field, thereby getting caught in the honey trap and allowing my code to detect the submission was not from a real user.
The spam bots have, as expected, gotten smarter and this technique no longer catches them out.
I recently launched a new site and was immediately flooded with spam pretty much as soon as it went live and was indexed by the various search engines.
My research into alternative techniques to combat spam led me to Google reCAPTHCA. Google uses intelligent techniques to detect if forms submissions are completed by real people or otherwise.
Once I added Google reCAPTCHA to my newly launched site, all my the spam I was received stopped immediately. Now sales can more easily rely on the fact that the submission are much more likely to be coming from real people and companies.
Is it 100% fool proof? I can't say definitively that bots cannot out smart it but at least with the backing of a company like Google, I can have some confidence that whist they continue to support it, they will continue to improve it and combat spammers.
For now, I can say that since I've implemented it, I've not seen any more spam coming through.
So, how can you do it?
To implement Google's reCAPTCHA yourself, there are a few basic steps you need to follow:
Register your site with Google's reCAPTCHA
Add the relevant script code to your pages that have any kind of forms
In your form submission code, verify with Google reCAPTCHA that the submission is genuine
So, let's get to it then.
To register your site to use Google's reCAPTCHA, first register for reCAPTCA at Google's reCAPTCHA Admin site, then register your site.
When registering your site, consider adding multiple domain names in the registration. In my case, I added my local test environment name as well as my production name. By doing this, you only need to worry about a single site secret and secret key. That said, I can see the value in having your development site and production site registered as different sites allowing your Google site statistics to be kept separate.
Important: When you register your site, take note of the Site Key and Site Secret. You will need these values later in your app settings so that are used in the Google reCAPTCHA implementation.
To implement it in your Umbraco site, the first place I started was to add the Site Key and Site Secret into the appsettings.json file. I created a new section in the file called "GoogleReCaptcha" and added the new keys and values to it:
1{2 "$schema": "./umbraco/config/appsettings-schema.json",3.4.5.6 "GoogleReCaptcha": {7 "SiteKey": "your reCAPTHCA site key",8 "SecretKey": "your reCAPTHCA site secret"9 },10.11.12.13}
Next, I created a new class that would be used to read those settings from the appsettings.json file:
1namespace Sample.Core.ViewModels2{3 public class GoogleReCaptchaSettings4 {5 public string SiteKey { get; set; }6 public string SecretKey { get; set; }78 }9}
Now, I added the code to read these settings and add it to the dependency injection container in the Startup.cs file. In the ConfigureServices method, add the following code:
1public void ConfigureServices(IServiceCollection services)2{3.4.5.6 services.Configure<GoogleReCaptchaSettings>(_config.GetSection("GoogleReCaptcha"));7.8.9.10 services.AddUmbraco(_env, _config)11 .AddBackOffice()12 .AddWebsite()13 .AddComposers()14 .Build();15}16
I can now use dependency injection to inject the class into my constructor of the classes where I need to reference the settings.
Next, we need to inject the instance of the settings class and add the relevant javascript reference passing in the Google reCAPTCHA site key to the view for your component with your form in your Umbraco solution.
Open your components .cshtml file and start by adding the following line somewhere near the top of the page:
1@inject IOptions<GoogleReCaptchaSettings> _googleCaptchaSettings;
This will use dependency injection to inject an IOptions instance of our GoogleReCaptchaSettings class into our page.
At the top of your form definition, inside your BeginUmbracoForm tage, add a hidden field that will be used to hold the token from Google reCAPTCHA:
1@Html.HiddenFor(m => m.Token)
Next, toward the bottom of the page under your form definition, add a script reference to the Google reCAPTCHA api to load it into your page, add the reference to your site key from your GoogleReCaptchaSettings instance.
1<script src="https://www.google.com/recaptcha/api.js?render=@_googleCaptchaSettings.Value.SiteKey"></script>2
The last change you need to make on your component view, is to add a new script block under this last script tag. This block do the following:
- define a function to add a handler that will be executed on submit to populate the hidden token field with the relevant value from Google reCAPTCHA
- set an time interval to have that code re-executed and the token value re-assigned with the value from Google as depending on how long the page is left open, the value from Google will update
- add an listener so that once the dom content is fully loaded, the function is executed
1<script>2 function getToken() {3 grecaptcha.ready(function() {4 grecaptcha.execute('@_googleCaptchaSettings.Value.SiteKey', {action: 'submit'})5 .then(function(token) {6 document.getElementById("Token").value = token;7 });8 });9 }1011 setInterval(getToken, 115000);1213 window.addEventListener('DOMContentLoaded', (event) => {14 getToken();15 });16</script>
When the form is now rendered, you will notice the Google reCAPTCHA badge displayed in the lower right corner of the page. This badge not only shows that Google reCAPTCHA is used on your site giving your end users some confidence in your site, but when hovered over, also the shows the links to Google's Privacy and Terms pages.
Now, once your form is submitted, your surface controller can use form value in the Token field and call Google's reCAPTCHA service to verify the token to determine if the submission was done by a real person or by some spam bot.
First, let's define our own service that we will use to interact with Google reCAPTCHA. This service can then be injected into our SurfaceController using dependency injection.
We'll define our service interface first. It only needs one method that will be used to verify the token:
1using System.Threading.Tasks;23namespace Sample.Core.Interfaces4{5 public interface IGoogleCaptchaService6 {7 Task<bool> VerifyToken(string token);8 }9}
Now the implementation of that interface:
1using Microsoft.AspNetCore.Http;2using Microsoft.Extensions.Options;3using Newtonsoft.Json;4using Sample.Core.Interfaces;5using Sample.Core.ViewModels;6using System.Net.Http;7using System.Threading.Tasks;89namespace Sample.Core.Services10{11 public class GoogleCaptchaService : IGoogleCaptchaService12 {13 private readonly GoogleCaptchaSettings _googleCaptchaSettings;14 private readonly IHttpContextAccessor _httpContext;1516 public GoogleCaptchaService(IOptions<GoogleCaptchaSettings> googleCaptchaSettings, IHttpContextAccessor httpContext)17 {18 _googleCaptchaSettings = googleCaptchaSettings.Value;19 _httpContext = httpContext;20 }2122 public async Task<bool> VerifyToken(string token)23 {24 try25 {26 // Post to: https://www.google.com/recaptcha/api/siteverify27 // Parameters: secret, response, remoteip2829 var remoteIp = _httpContext.HttpContext.Connection.RemoteIpAddress;30 var url = $"https://www.google.com/recaptcha/api/siteverify?secret={_googleCaptchaSettings.SecretKey}&response={token}&remoteip={remoteIp}";3132 using (var client = new HttpClient())33 {34 var httpResult = await client.GetAsync(url);35 if (!httpResult.IsSuccessStatusCode)36 return false;3738 var responseString = await httpResult.Content.ReadAsStringAsync();39 var googleResult = JsonConvert.DeserializeObject<GoogleCaptchaResponse>(responseString);4041 return googleResult.Success && googleResult.Score >= 0.5;42 }43 }44 catch (System.Exception)45 {46 return false;47 }48 }49 }50}
The VerifyToken method accepts the token value that was submitted with your form. It calls Google reCAPTHCA service to verify the token. The response message is used to determine if it is a valid token.
Now, we need to register this service in a composer so that an instance of this service can be passed to our SurfaceController via dependency injection.
1using Microsoft.Extensions.DependencyInjection;2using Sample.Core.Interfaces;3using Sample.Core.Services;4using Umbraco.Cms.Core.Composing;5using Umbraco.Cms.Core.DependencyInjection;67namespace Sample.Core.Composers8{9 public class RegisterDependencies : IComposer10 {11 public void Compose(IUmbracoBuilder builder)12 {13 .14 .15 .16 builder.Services.AddTransient<IGoogleCaptchaService, GoogleCaptchaService>();17 .18 .19 .20 }21 }22}
Lastly, we can now update our SurfaceController to accept an instance of our new service and then use that instance to verify the token when our forms are submitted.
In the example below, if the submission is verified by Google, I then email the contents of the submission. I then return the user to the same and add a value to the TempData collection to indicate if the email was sent succesfully.
On the component, I then display a relevant message. This code has not been included.
1using Microsoft.AspNetCore.Mvc;2using MimeKit;3using System.Text;4using System;5using Umbraco.Cms.Core.Cache;6using Umbraco.Cms.Core.Logging;7using Umbraco.Cms.Core.Routing;8using Umbraco.Cms.Core.Services;9using Umbraco.Cms.Core.Web;10using Umbraco.Cms.Infrastructure.Persistence;11using Umbraco.Cms.Web.Common.Filters;12using Umbraco.Cms.Web.Website.Controllers;13using Sample.Core.ViewModels.Components;14using Sample.Core.Interfaces;15using System.Threading.Tasks;16using Microsoft.Extensions.Options;17using Sample.Core.ViewModels;18using System.Collections.Generic;1920namespace Sample.Core.Controllers.Umbraco21{22 public class FormController : SurfaceController23 {24 private readonly IGoogleCaptchaService _googleCaptchaService;25 private readonly ISendGridService _sendGridService;26 private readonly SendGridSettingsOptions _sendGridSettings;2728 public FormController(IUmbracoContextAccessor umbracoContextAccessor,29 IUmbracoDatabaseFactory databaseFactory,30 ServiceContext services,31 AppCaches appCaches,32 IProfilingLogger profilingLogger,33 IPublishedUrlProvider publishedUrlProvider,34 IGoogleCaptchaService googleCaptchaService,35 ISendGridService sendGridService,36 IOptions<SendGridSettingsOptions> sendGridSettings) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)37 {38 _googleCaptchaService = googleCaptchaService;39 _sendGridService = sendGridService;40 _sendGridSettings = sendGridSettings.Value;41 }4243 [HttpPost]44 [ValidateUmbracoFormRouteString]45 public async Task<IActionResult> ContactForm(ContactFormViewModel model)46 {47 // Verify the response token with Google48 if (!await _googleCaptchaService.VerifyToken(model.Token))49 {50 // If we have caught the spammer/bot, let them think they have been successful51 TempData["success"] = true;52 return RedirectToCurrentUmbracoPage();53 }5455 if (!ModelState.IsValid)56 {57 return CurrentUmbracoPage();58 }5960 // Ok, we've determined the submission is valid so let's send the email now6162 MimeMessage message = new MimeMessage();63 MailboxAddress to = new MailboxAddress(_sendGridSettings.To, _sendGridSettings.To);64 message.To.Add(to);65 message.Subject = "Sample Contact Us";6667 StringBuilder builder = new StringBuilder();68 builder.Append($"<h1>A new contact us forms has been submitted.</h1><br /><br />");69 builder.Append($"<strong>Name: </strong>{model.Name}<br />");70 builder.Append($"<strong>Email: </strong>{model.Email}<br />");71 builder.Append($"<strong>Coment:</strong><br />{model.Comments}<br />");7273 BodyBuilder bodyBuilder = new BodyBuilder();74 bodyBuilder.HtmlBody = builder.ToString();7576 message.Body = bodyBuilder.ToMessageBody();7778 if (await _sendGridService.SendMail(message))79 {80 TempData["success"] = true;81 return RedirectToCurrentUmbracoPage();82 }83 else84 {85 TempData["failed"] = true;86 return RedirectToCurrentUmbracoPage();87 }88 }89 }90}