ASP.NET Core 5.0 - Is Human with Google reCAPTCHA

Ken Haggerty
Created 01/06/2021 - Updated 01/06/2021 06:04

This article will describe the implementation of Google reCAPTCHA v3. I will assume you have downloaded the ASP.NET Core 5.0 - Homegrown Analytics Project or created a new ASP.NET Core 5.0 Razor Pages project. See Tutorial: Get started with Razor Pages in ASP.NET Core.

Homegrown Analytics Project and Article Series

The project is feature complete and will increment the version with updates. The details page includes the version change log. This project is designed and organized to allow easy integration with existing projects. The KenHaggerty. Com. SingleUser NuGet package segregates the user logic. SQL scripts to create the schema, tables, and default data are included in the package. Details, screenshots, and related articles can be found at ASP.NET Core 5.0 - Homegrown Analytics Project.

Before I implemented Google reCAPTCHA v3 on the contact and register pages, I was getting emails and users created by robots. Since, I haven't noticed any. The project has an option to enable the reCAPTCHA demonstration. The Startup class has static properties for options.

Startup.cs:
// Enables the Google Recaptcha demo.
// Requires a registered key and secret set in appsettings.json for the
// GoogleRecaptchaKey and GoogleRecaptchaSecret properties.
public static bool EnableGoogleRecaptcha { get; } = true;

You must register your site to receive a reCAPTCHA v3 key and secret at Google reCAPTCHA v3 - Register a new site. You set the secret, key, and a pass/fail threshold in appsettings.json. reCAPTCHA v3 returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot).

appsettings.json:
"GoogleRecaptcha": {
  "Key": "GoogleRecaptchaKey",
  "Secret": "GoogleRecaptchaSecret",
  "ScoreThreshold": "0.5"
},

The GoogleRecaptcha Key is required to initialize the process. The project injects the application settings with IConfiguration on the Captcha Result page. The OnGet loads the GoogleRecaptcha Key from settings to a GoogleRecaptchaKey property.

Admin > Analytics > CaptchaResult > OnGet:
var googleRecaptcha = _configuration.GetSection("GoogleRecaptcha").GetChildren();
foreach (var item in googleRecaptcha)
{
    if (item.Key == "Key")
    {
        GoogleRecaptchaKey = item.Value;
        break;
    }
}

JavaScript employs the Model. GoogleRecaptchaKey to acquire a challenge token. The token is passed to a Challenge method on the server.

Admin > Analytics > CaptchaResult.cshtml:
<script src="https://www.google.com/recaptcha/api.js?render=@Model.GoogleRecaptchaKey"></script>
<script>
    grecaptcha.ready(function () {
        grecaptcha.execute('@Model.GoogleRecaptchaKey', { action: 'Contact' })
            .then(function (token) {
                return fetch('/admin/analytics/captcharesult/challenge?token=' + token,
                    { method: 'get', credentials: 'same-origin', headers: { 'Content-Type': 'application/json;charset=UTF-8' } })
                    .then(function (response) {
                        if (response.ok) return response.text();
                        else throw Error('Response Not OK');
                    })
                    .then(function (text) {
                        try { return JSON.parse(text); }
                        catch (err) { throw Error('Method Not Found'); }
                    })
                    .then(function (responseJSON) {
                        if (responseJSON.Human)
                            window.location.href = window.location.protocol + "//"
                                + window.location.host + window.location.pathname + '?human=true';
                        else {
                            document.querySelector('.spinner-div').classList.add('d-none');
                            document.querySelector('.error-div').classList.remove('d-none');
                            document.querySelector('.captcha-result').innerHTML = JSON.stringify(responseJSON, null, 2);
                        }
                    })
                    .catch(function (error) {
                        document.querySelector('.spinner-div').classList.add('d-none');
                        document.querySelector('.error-div').classList.remove('d-none');
                        document.querySelector('.captcha-result').innerHTML = error;
                    })
            });
    });
</script>

The project implements a named System. Net. Http. HttpClient.

Startup > ConfigureServices:
if (EnableGoogleRecaptcha)
{
    services.AddHttpClient("googleRecaptcha", client =>
    {
        client.BaseAddress = new Uri("https://www.google.com/recaptcha/api/siteverify");
        client.DefaultRequestVersion = new Version(2, 0);
    });
}

The Challenge function parses the configuration settings for the GoogleRecaptcha Secret and ScoreThreshold. The injected IHttpClientFactory initializes the googleRecaptcha client. The secret and token are required by the API to request a response. The score is parsed from the response and compared to the ScoreThreshold. The Challenge function returns a JsonResult indicating the challenge status and a bool Human property.

Admin > Analytics > CaptchaResult.cshtml.cs:
public async Task<JsonResult> OnGetChallengeAsync(string token)
{
    var googleRecaptcha = _configuration.GetSection("GoogleRecaptcha").GetChildren();
    string secret = string.Empty;
    decimal scoreThreshold = 0; //scoreThreshold = 0.5M;         
    foreach (var item in googleRecaptcha)
    {
        if (item.Key == "Key") continue;
        else if (item.Key == "Secret") secret = item.Value;
        else if (item.Key == "ScoreThreshold") scoreThreshold = decimal.Parse(item.Value);
        else return new JsonResult(new { Status = "Failed", Errors = "GoogleRecaptcha configuration error." });
    }

    var client = _clientFactory.CreateClient("googleRecaptcha");
    var response = await client.GetStringAsync($"?secret={secret}&response={token}").ConfigureAwait(false);

    var tokenResponse = JsonSerializer.Deserialize<CaptchaTokenResponse>(response);

    var human = tokenResponse.Score > scoreThreshold;
    // Test view with human = false;

    var captchaChallenge = new CaptchaChallenge(tokenResponse, human);
    var result = await _analyticsService.AddCaptchaChallengeAsync(captchaChallenge).ConfigureAwait(false);
    if (!result.Succeeded) return new JsonResult(new
    {
        Status = "Failed",
        Errors = $"An error occurred adding a CaptchaChallenge ({result})."
    });
    if (captchaChallenge.Id == 0) return new JsonResult(new
    {
        Status = "Failed",
        Errors = "An error occurred adding a CaptchaChallenge (captchaChallenge.Id = 0)."
    });
    if (!captchaChallenge.Success) return new JsonResult(new
    {
        Status = "Failed",
        Errors = "An error occurred adding a CaptchaChallenge (captchaChallenge.Success = false)."
    });

    return new JsonResult(captchaChallenge);
}

The response is deserialized using System. Text. Json into a CaptchaTokenResponse model.

Entities > CaptchaTokenResponse.cs:
public class CaptchaTokenResponse
{
    [JsonPropertyName("success")]
    public bool Success { get; set; }
    [JsonPropertyName("score")]
    public decimal Score { get; set; }
    [JsonPropertyName("action")]
    public string Action { get; set; }
    [JsonPropertyName("challenge_ts")]
    public DateTime ChallengeDate { get; set; }
    [JsonPropertyName("hostname")]
    public string HostName { get; set; }
    [JsonPropertyName("error-codes")]
    public List<string> ErrorCodes { get; set; }
}

The CaptchaTokenResponse and a human parameter create a CaptchaChallenge entity. The project implements a Captcha Challenges listing page.

CaptchaChallenges Listing Page
CaptchaChallenges Listing Page Mobile

The project demonstrates Google reCAPTCHA v3 with a mock contact form.

Validating

Captcha Result Page Validating
Captcha Result Page Mobile Validating

Human

Captcha Result Page Passed
Captcha Result Page Mobile Passed

Completed

Captcha Result Page Completed
Captcha Result Page Mobile Completed

Not Human

Captcha Result Page Failed
Captcha Result Page Mobile Failed

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications