ASP.NET Core 5.0 - Is Human with Google reCAPTCHA

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.
Ken Haggerty
Created 01/06/21
Updated 01/06/21 06:04 GMT

Log In or Reset Quota to read more.

Successfully completed. Thank you for contributing.
Processing...
Something went wrong. Please try again.
Contribute to enjoy content without advertisments.
You can contribute without registering.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?