Google reCAPTCHA 2.0 in ASP.NET MVC einbinden und verwenden

Das Google ReCaptcha bietet eine gute Möglichkeit mit der Bots und Skripte von Eingaben auf einer Webseite abgehalten werden können. Um das Captcha auf der eigenen Webseite, in diesem Fall ASP.NET MVC, einbinden zu können, sind jedoch einige Schritte notwendig. Zunächst einmal, muss man sich einen privaten und öffentlichen Schlüssel für die Google reCAPTCHA 2.0 API besorgen. Dies geht unter: https://www.google.com/recaptcha

Sobald man die beiden benötigten Schlüssel erhalten hat, kann mit der Implementierung begonnen werden. 

Beginn der Implementierung

Angekommen in unserem Projekt müssen wir – möglichst früh im Quellcode – das JavaScript von Google laden. Dies geht mit der folgenden Zeile:

<script src='https://www.google.com/recaptcha/api.js'></script>

Nun können wir an der benötigten Stelle unserer Webseite das Captcha platzieren. Damit keine Verwirrung aufkommt: Ich habe meine beiden Schlüssel in den Anwendungskonfigurationen unter den Bezeichnern „ReCaptcha_PublicKey“ sowie „ReCaptcha_PrivateKey“ abgelegt.  Ebenfalls möchte ich noch anmerken, dass ich meine Controller mittels Knockout und JavaScript-Ajax anspreche. Sollten hier klassische HTML/ASP-Form-Elemente verwendet werden, muss die Logik im späteren Verlauf entsprechend angepasst werden.

Um das Captcha vollständig einzubinden, benötigen wir nur folgende Zeile Code, welche sich idealerweise bei unserer Eingabe (oder im entsprechenden Form-Element) befindet.

<div id="captcha" class="g-recaptcha" data-sitekey="@Settings.Default.ReCaptcha_PublicKey"></div>

Die Daten befinden sich am Ende in einem Element mit dem Namen bzw.  der ID „g-recaptcha-response“. In meinem Fall lese ich diese im JavaScript mittels jQuery aus und übermittle diese anschließend per Ajax an meinen Controller im Model zusammen mit den anderen Daten meiner Funktionalität.

Das Model und die reCAPTCHA Logik

Kommen wir zu unserem Daten-Model sowie zur Logik. Schließlich wollen wir unser Captcha ja auch verifizieren. Zunächst einmal verwende ich eine abstrakte Basisklasse für mein Model, von welcher all meine Datenmodels ableiten, wo das Captcha verwendet wird. Diese sieht in meinem Fall wie folgt aus:

public abstract class CaptchaDataBase
{
    public string CaptchaData { get; set; }
}

Um die Verifizierung des Captchas nun anzustoßen, verwende ich folgende Klasse (Attribut):

public class ReCaptchaAttribute : ActionFilterAttribute
{
    public static string ModelStateErrorKey => "ReCaptcha";

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var userIP = filterContext.RequestContext.HttpContext.Request.UserHostAddress;
        foreach (var pair in filterContext.ActionParameters)
        {
            var dataBase = pair.Value as CaptchaDataBase;
            if (dataBase != null)
            {
                using (WebClient client = new WebClient())
                {
                    string reply = client.DownloadString($"https://www.google.com/recaptcha/api/siteverify?secret={Settings.Default.ReCaptcha_PrivateKey}&response={dataBase.CaptchaData}&remoteip={userIP}");
                    var captchaResponse = JsonConvert.DeserializeObject<CaptchaResponse>(reply);

                    if (!captchaResponse.Success)
                    {
                        if (captchaResponse.ErrorCodes.Count <= 0)
                        {
                            ((Controller)filterContext.Controller).ModelState.AddModelError(ModelStateErrorKey, string.Empty);
                            return;
                        }

                        string responseError;
                        switch (captchaResponse.ErrorCodes[0].ToLower())
                        {
                            case ("missing-input-secret"):
                                responseError = "The secret parameter is missing.";
                                break;
                            case ("invalid-input-secret"):
                                responseError = "The secret parameter is invalid or malformed.";
                                break;

                            case ("missing-input-response"):
                                responseError = "The response parameter is missing.";
                                break;
                            case ("invalid-input-response"):
                                responseError = "The response parameter is invalid or malformed.";
                                break;

                            default:
                                responseError = "Error occured. Please try again";
                                break;
                        }


                        ((Controller)filterContext.Controller).ModelState.AddModelError(ModelStateErrorKey, responseError);
                        return;
                    }
                }
            }
        }
    }

    public class CaptchaResponse
    {
        [JsonProperty("success")]
        public bool Success { get; set; }

        [JsonProperty("error-codes")]
        public List<string> ErrorCodes { get; set; }
    }
}

Diese wird in der entsprechenden Controller-Action über ein Attribut (ReCaptcha) eingebunden und automatisch bei Aufruf ausgelöst. Zu Beginn meiner Action  frage ich jetzt nur noch ab, ob das Captcha einen „OK“-Status besitzt.

[HttpPost, ReCaptcha]
public ActionResult RegisterUser(RegisterData data)
{
    try
    {
        if (!ModelState.IsValid)
            return Json(new ResultDataMapper(409, "Captcha invalid!", ModelState[ReCaptchaAttribute.ModelStateErrorKey]?.Errors[0]?.ErrorMessage));

        //some implementation

        return Json(new ResultDataMapper());
    }
    catch (Exception e)
    {
        Log.Error(e.Message, e);
        return Json(new ResultDataMapper(500, e.Message));
    }
}

Schlusswort

Ich hoffe damit ist wieder einigen geholfen, denn das Google reCAPTCHA 2.0 bietet eine ideale Möglichkeit, um sich von allerlei Unfug zu befreien. Viele große Anbieter wie WordPress, Typo3, etc. bieten für die Google reCAPTCHA Implementierung viele Erweiterungen an, aber es schadet ja auch nie, selbst zu wissen wie das ganze funktioniert.

Edit / Anmerkung – Mit ASP-FormAuthentication

Aufgrund der Nachfrage, hier noch die Version der ReCaptchaAttribute-Klasse, wenn mit der Standard ASP-FormAuthentication gearbeitet wird:

public class ReCaptchaAttribute : ActionFilterAttribute
{
    public static string ModelStateErrorKey => "ReCaptcha";

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var userIP = filterContext.RequestContext.HttpContext.Request.UserHostAddress;
        var response = filterContext.RequestContext.HttpContext.Request["g-recaptcha-response"];
            
        using (WebClient client = new WebClient())
        {
            string reply = client.DownloadString($"https://www.google.com/recaptcha/api/siteverify?secret={Settings.Default.ReCaptcha_PrivateKey}&response={response}&remoteip={userIP}");
            var captchaResponse = JsonConvert.DeserializeObject<CaptchaResponse>(reply);

            if (!captchaResponse.Success)
            {
                if (captchaResponse.ErrorCodes.Count <= 0)
                {
                    ((Controller)filterContext.Controller).ModelState.AddModelError(ModelStateErrorKey, string.Empty);
                    return;
                }

                string responseError;
                switch (captchaResponse.ErrorCodes[0].ToLower())
                {
                    case ("missing-input-secret"):
                        responseError = "The secret parameter is missing.";
                        break;
                    case ("invalid-input-secret"):
                        responseError = "The secret parameter is invalid or malformed.";
                        break;

                    case ("missing-input-response"):
                        responseError = "The response parameter is missing.";
                        break;
                    case ("invalid-input-response"):
                        responseError = "The response parameter is invalid or malformed.";
                        break;

                    default:
                        responseError = "Error occured. Please try again";
                        break;
                }


                ((Controller)filterContext.Controller).ModelState.AddModelError(ModelStateErrorKey, responseError);
            }
        }
    }

    public class CaptchaResponse
    {
        [JsonProperty("success")]
        public bool Success { get; set; }

        [JsonProperty("error-codes")]
        public List<string> ErrorCodes { get; set; }
    }
}

Im Controller wird weiterhin nur das Attribut gesetzt und der ModelState überprüft.