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><head></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><div></code> that is created by the <see cref="BodyDivInclude"/>.
/// </summary>
public List<string> ExtraClasses { get { return _ExtraClasses; } }
/// <summary>
/// Returns the <code><div></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: