6
\$\begingroup\$

This is a follow-up to the other post I made (less than an hour ago) about my Google reCAPTCHA C# implementation: Google reCAPTCHA Validator

This adds support for error messages, so that you can determine why a reCAPTCHA request failed (if an error was returned).

This also fixes a bug if you were to use the ReCaptchaLocationInclude before having set the ExtraClasses to a non-null list.

I added an enumeration for errors:

/// <summary>
/// Indicates errors that could be returned by the reCAPTCHA API.
/// </summary>
/// <remarks>
/// See: https://developers.google.com/recaptcha/docs/verify
/// </remarks>
[Flags]
public enum ReCaptchaErrors
{
    /// <summary>
    /// No errors occurred.
    /// </summary>
    None = 0x00,
    /// <summary>
    /// The secret parameter is missing.
    /// </summary>
    MissingInputSecret = 0x01,
    /// <summary>
    /// The secret parameter is invalid or malformed.
    /// </summary>
    InvalidInputSecret = 0x02,
    /// <summary>
    /// The response parameter is missing.
    /// </summary>
    MissingInputResponse = 0x04,
    /// <summary>
    /// The response parameter is invalid or malformed.
    /// </summary>
    InvalidInputResponse = 0x08,
}

A class for reCAPTCHA responses:

/// <summary>
/// This class is used by the <see cref="ReCaptchaValidator"/> to return a proper response to a reCAPTCHA validation request.
/// </summary>
public class ReCaptchaResponse
{
    private bool _Success;
    private ReCaptchaErrors _Errors;

    /// <summary>
    /// Returns a value indicating if the <see cref="ReCaptchaValidator"/> succeeded in validating the reCAPTCHA response or not.
    /// </summary>
    public bool Success { get { return _Success; } }

    /// <summary>
    /// Returns any <see cref="ReCaptchaErrors"/> that occurred during the reCAPTCHA response validation.
    /// </summary>
    public ReCaptchaErrors Errors { get { return _Errors; } }

    /// <summary>
    /// Creates a new <see cref="ReCaptchaResponse"/> from the specified JSON string.
    /// </summary>
    /// <param name="jsonResponse">The JSON string to transform.</param>
    public ReCaptchaResponse(string jsonResponse)
    {
        JavaScriptSerializer jss = new JavaScriptSerializer();
        dynamic deserializedJson = jss.DeserializeObject(jsonResponse);

        _Success = deserializedJson["success"];
        _Errors = ReCaptchaErrors.None;

        if (deserializedJson.ContainsKey("error-codes"))
        {
            foreach (string error in deserializedJson["error-codes"])
            {
                // Our `ReCaptchaErrors` enum contains the exact same strings as the returned `error` text would be, with the following transformations:
                // 1. The words are transformed to PascalCase;
                // 2. The dashes are stripped;
                string[] errorWords = error.Split('-');

                string errorEnumName = "";
                foreach (string errorWord in errorWords)
                    errorEnumName += errorWord[0].ToString().ToUpper() + errorWord.Substring(1);

                _Errors = _Errors | (ReCaptchaErrors)Enum.Parse(typeof(ReCaptchaErrors), errorEnumName);
            }
        }
    }
}

And updated the reCAPTCHA validator:

/// <summary>
/// This class provides the ability to easily implement Google's reCAPTCHA.
/// </summary>
/// <remarks>
/// See: https://www.google.com/recaptcha/intro/index.html
/// </remarks>
public class ReCaptchaValidator
{
    private const string _HeadScriptInclude = "<script src='https://www.google.com/recaptcha/api.js'></script>";
    private const string _BodyDivInclude = "<div class=\"g-recaptcha %EXTRACLASSES%\" data-sitekey=\"%SITEKEY%\"></div>";
    private const string _ReCaptchaFormCode = "g-recaptcha-response";

    private readonly string _ReCaptchaSecret;
    private readonly string _ReCaptchaSiteKey;
    private readonly List<string> _ExtraClasses = new List<string>();

    /// <summary>
    /// Returns the script to be included in the <code>&lt;head&gt;</code> of the page.
    /// </summary>
    public string HeadScriptInclude { get { return _HeadScriptInclude; } }

    /// <summary>
    /// Use this to get or set any extra classes that should be added to the <code>&lt;div&gt;</code> that is created by the <see cref="BodyDivInclude"/>.
    /// </summary>
    public List<string> ExtraClasses { get { return _ExtraClasses; } }

    /// <summary>
    /// Returns the <code>&lt;div&gt;</code> that should be inserted in the HTML where the reCAPTCHA should go.
    /// </summary>
    /// <remarks>
    /// I'm still not sure if this should be a method or not.
    /// </remarks>
    public string BodyDivInclude { get { return _BodyDivInclude.Replace("%SITEKEY%", _ReCaptchaSiteKey).Replace("%EXTRACLASSES%", string.Join(" ", ExtraClasses)); } }

    /// <summary>
    /// Creates a new instance of the <see cref="ReCaptchaValidator"/>.
    /// </summary>
    /// <param name="reCaptchaSecret">The reCAPTCHA secret.</param>
    /// <param name="reCaptchaSiteKey">The reCAPTCHA site key.</param>
    public ReCaptchaValidator(string reCaptchaSecret, string reCaptchaSiteKey)
    {
        _ReCaptchaSecret = reCaptchaSecret;
        _ReCaptchaSiteKey = reCaptchaSiteKey;
    }

    /// <summary>
    /// Determines if the reCAPTCHA response in a <code>NameValueCollection</code> passed validation.
    /// </summary>
    /// <param name="form">The <code>Request.Form</code> to validate.</param>
    /// <returns>A <see cref="ReCaptchaResponse"/> value indicating the response of the verification.</returns>
    public ReCaptchaResponse Validate(NameValueCollection form)
    {
        string reCaptchaSecret = _ReCaptchaSecret;
        string reCaptchaResponse = form[_ReCaptchaFormCode];

        using (WebClient client = new WebClient())
        {
            // TODO: send user's IP optionally with reCAPTCHA information.
            byte[] response = client.UploadValues("https://www.google.com/recaptcha/api/siteverify",
                                                    new NameValueCollection() { { "secret", reCaptchaSecret }, { "response", reCaptchaResponse } });

            string reCaptchaResult = System.Text.Encoding.UTF8.GetString(response);

            return new ReCaptchaResponse(reCaptchaResult);
        }
    }
}

For the updated ReCaptchaValidator, I changed the ReCaptchaLocationInclude property to BodyDivInclude as well, to make the name more meaningful. I also made ExtraClasses a get-only List, so that you cannot accidentally assign a new list to it. (You'll just have to use ReCaptchaValidator.ExtraClasses.Clear() first and then add your new classes.)

Usage:

string reCaptchaSecret = "";
string reCaptchaSiteKey = "";

protected void Page_Load(object sender, EventArgs e)
{
    ReCaptchaValidator rcv = new ReCaptchaValidator(reCaptchaSecret, reCaptchaSiteKey);
    reCaptchaBodyCode.Text = rcv.BodyDivInclude;
    reCaptchaHeadCode.Text = rcv.HeadScriptInclude;
}

protected void CreateUser_Click(object sender, EventArgs e)
{
    ReCaptchaValidator rcv = new ReCaptchaValidator(reCaptchaSecret, reCaptchaSiteKey);
    ReCaptchaResponse reCaptchaResponse = rcv.Validate(Request.Form);

    if (reCaptchaResponse.Success)
    {
        // Please do not comment on code within this block, it is provided as-is by Microsoft and I will not change it unless it is broken (and it is not).

        var manager = Context.GetOwinContext().GetUserManager<ApplicationUserManager>();
        var signInManager = Context.GetOwinContext().Get<ApplicationSignInManager>();
        var user = new ApplicationUser() { UserName = Email.Text, Email = Email.Text };
        IdentityResult result = manager.Create(user, Password.Text);
        if (result.Succeeded)
        {
            // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
            //string code = manager.GenerateEmailConfirmationToken(user.Id);
            //string callbackUrl = IdentityHelper.GetUserConfirmationRedirectUrl(code, user.Id, Request);
            //manager.SendEmail(user.Id, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>.");

            signInManager.SignIn(user, isPersistent: false, rememberBrowser: false);
            IdentityHelper.RedirectToReturnUrl(Request.QueryString["ReturnUrl"], Response);
        }
        else
        {
            ErrorMessage.Text = result.Errors.FirstOrDefault();
        }
    }
    else
    {
        ErrorMessage.Text = "We could not verify that you are a human.";
    }
}

This shows the reCAPTCHA being implemented on the registration page of a default ASP.NET Web Forms application. The block of code inside the if (reCaptchaResponse.Success) block is the default code for the page, the rest of it is my modifications. Please do not comment on code within that block.

And lastly, the front-end:

<asp:Content runat="server" ID="HeadContent" ContentPlaceHolderID="HeadContent">
    <%-- This next line is where the <script> tag will end up. --%>
    <asp:Literal ID="reCaptchaHeadCode" runat="server"></asp:Literal>
</asp:Content>

<asp:Content runat="server" ID="BodyContent" ContentPlaceHolderID="MainContent">
    <h2><%: Title %>.</h2>
    <p class="text-danger">
        <asp:Literal runat="server" ID="ErrorMessage" />
    </p>

    <div class="form-horizontal">
        <h4>Create a new account</h4>
        <hr />
        <asp:ValidationSummary runat="server" CssClass="text-danger" />
        <div class="form-group">
            <asp:Label runat="server" AssociatedControlID="Email" CssClass="col-md-2 control-label">Email</asp:Label>
            <div class="col-md-10">
                <asp:TextBox runat="server" ID="Email" CssClass="form-control" TextMode="Email" />
                <asp:RequiredFieldValidator runat="server" ControlToValidate="Email"
                    CssClass="text-danger" ErrorMessage="The email field is required." />
            </div>
        </div>
        <div class="form-group">
            <asp:Label runat="server" AssociatedControlID="Password" CssClass="col-md-2 control-label">Password</asp:Label>
            <div class="col-md-10">
                <asp:TextBox runat="server" ID="Password" TextMode="Password" CssClass="form-control" />
                <asp:RequiredFieldValidator runat="server" ControlToValidate="Password"
                    CssClass="text-danger" ErrorMessage="The password field is required." />
            </div>
        </div>
        <div class="form-group">
            <asp:Label runat="server" AssociatedControlID="ConfirmPassword" CssClass="col-md-2 control-label">Confirm password</asp:Label>
            <div class="col-md-10">
                <asp:TextBox runat="server" ID="ConfirmPassword" TextMode="Password" CssClass="form-control" />
                <asp:RequiredFieldValidator runat="server" ControlToValidate="ConfirmPassword"
                    CssClass="text-danger" Display="Dynamic" ErrorMessage="The confirm password field is required." />
                <asp:CompareValidator runat="server" ControlToCompare="Password" ControlToValidate="ConfirmPassword"
                    CssClass="text-danger" Display="Dynamic" ErrorMessage="The password and confirmation password do not match." />
            </div>
        </div>
        <%-- This next line is where the body code will be filled. --%>
        <asp:Literal ID="reCaptchaBodyCode" runat="server"></asp:Literal>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <asp:Button runat="server" OnClick="CreateUser_Click" Text="Register" CssClass="btn btn-default" />
            </div>
        </div>
    </div>
</asp:Content>

Please do not comment too much on the markup, it's provided more for context and to show that usage is actually quite simple.

I do not mind if anyone uses this, just please credit this post as the original source for the code.

Also, here are a couple of images with it in action:

Code Side of reCAPTCHA Validation Client Side of reCAPTCHA Validation

\$\endgroup\$
4
\$\begingroup\$

It's not considered good form to have operations in the constructor that can throw exceptions - it should simply validate parameters (and exception throwing can be done by you) and then the operations should take place in a method such as Process or what-not. Though in the case of ReCaptchaResponse, it might be a simple static class:

/// <summary>
/// This class is used by the <see cref="ReCaptchaValidator"/> to return a proper response to a reCAPTCHA validation request.
/// </summary>
public static class ReCaptchaResponse
{
    /// <summary>
    /// Creates a new <see cref="ReCaptchaResponse"/> from the specified JSON string.
    /// </summary>
    /// <param name="jsonResponse">The JSON string to transform.</param>
    /// <param name="errors">Returns any <see cref="ReCaptchaErrors"/> that occurred during the reCAPTCHA response validation.</param>
    /// <returns>Returns a value indicating if the <see cref="ReCaptchaValidator"/> succeeded in validating the reCAPTCHA response or not.</returns>
    public static bool TryParseJson(string jsonResponse, out ReCaptchaErrors errors)
    {
        JavaScriptSerializer jss = new JavaScriptSerializer();
        dynamic deserializedJson = jss.DeserializeObject(jsonResponse);

        bool success = deserializedJson["success"];
        errors = ReCaptchaErrors.None;

        if (deserializedJson.ContainsKey("error-codes"))
        {
            foreach (string error in deserializedJson["error-codes"])
            {
                // Our `ReCaptchaErrors` enum contains the exact same strings as the returned `error` text would be, with the following transformations:
                // 1. The words are transformed to PascalCase;
                // 2. The dashes are stripped;
                string[] errorWords = error.Split('-');

                string errorEnumName = "";
                foreach (string errorWord in errorWords)
                    errorEnumName += errorWord[0].ToString().ToUpper() + errorWord.Substring(1);

                errors = errors | (ReCaptchaErrors)Enum.Parse(typeof(ReCaptchaErrors), errorEnumName);
            }
        }

        return success;
    }
}

possibly make it an extension method by adding this to the first parameter and it can be invoked as such:

ReCaptchaErrors errors;

var success = reCaptchaResult.TryParseJson(out errors);
\$\endgroup\$
4
\$\begingroup\$
[Flags]
public enum ReCaptchaErrors
{
    /// <summary>
    /// No errors occurred.
    /// </summary>
    None = 0x00,
    /// <summary>
    /// The secret parameter is missing.
    /// </summary>
    MissingInputSecret = 0x01,
    /// <summary>
    /// The secret parameter is invalid or malformed.
    /// </summary>
    InvalidInputSecret = 0x02,
    /// <summary>
    /// The response parameter is missing.
    /// </summary>
    MissingInputResponse = 0x04,
    /// <summary>
    /// The response parameter is invalid or malformed.
    /// </summary>
    InvalidInputResponse = 0x08,
}

It appears you are assigning these values so the binary values of the enum options are 0001, 0010, 0100, 1000, etc... This is so you an assign multiple errors to a single variable, correct?

It would be clearer what exactly you are trying to do if you used bitshifting, like this:

[Flags]
public enum ReCaptchaErrors
{
    /// <summary>
    /// No errors occurred.
    /// </summary>
    None = 0,
    /// <summary>
    /// The secret parameter is missing.
    /// </summary>
    MissingInputSecret = 1 << 0,
    /// <summary>
    /// The secret parameter is invalid or malformed.
    /// </summary>
    InvalidInputSecret = 1 << 1,
    /// <summary>
    /// The response parameter is missing.
    /// </summary>
    MissingInputResponse = 1 << 2,
    /// <summary>
    /// The response parameter is invalid or malformed.
    /// </summary>
    InvalidInputResponse = 1 << 3,
}

Now, you can't accidentally break your code by typing 0x09 instead of 0x08, and it is immediately clear that you want each flag to be its own bit. For more information, you can read this blog post by our own @nhgrif.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.