PaulMiami / reCAPTCHA

reCAPTCHA 2.0 for ASPNET Core
MIT License
134 stars 37 forks source link

Razor Pages Support-Example #22

Open favance opened 7 years ago

favance commented 7 years ago

First, thanks for your contribution.

Is there support for .NET Core 2.0 Razor Pages? Is an example implementation available?

Fleximex commented 7 years ago

reCAPTCHA makes use of its own [ValidateRecaptcha] (that you put above a method and should check the captcha) which I didn't manage to get working. It seems like no one has written tutorials yet for Razor Pages. All tutorials assume you have controllers, while with .net core 2.0 and using Razor Pages you can basically use PageModels instead of controllers.

I'll share my code.

This is a method you put in the PageModel of the page you want to use your Captcha on which will verify your the Captcha with the Google servers:

private async Task<CaptchaVerification> VerifyCaptcha()
{
    string userIP = string.Empty;
    var ipAddress = Request.HttpContext.Connection.RemoteIpAddress;
    if (ipAddress != null) userIP = ipAddress.MapToIPv4().ToString();

    var captchaResponse = Request.Form["g-recaptcha-response"];
    var payload = string.Format("&secret={0}&remoteip={1}&response={2}",      
        "YOUR_PRIVATE_KEY",
        userIP,
        captchaResponse
        );

    var client = new HttpClient();
    client.BaseAddress = new Uri("https://www.google.com");
    var request = new HttpRequestMessage(HttpMethod.Post, "/recaptcha/api/siteverify");
    request.Content = new StringContent(payload, Encoding.UTF8, "application/x-www-form-urlencoded");
    var response = await client.SendAsync(request);
    return JsonConvert.DeserializeObject<CaptchaVerification>(response.Content.ReadAsStringAsync().Result);
}

Replace YOUR_PRIVATE_KEY with the private key you got from Google.

JsonConvert.DeserializeObject<CaptchaVerification> uses a custom class called CaptchaVerification which you also need. It can either be a separate class or a class inside your PageModel. This class essentially is the response from Google in which you encapsulate the response:

public class CaptchaVerification
{
    public CaptchaVerification()
    {
    }

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

    [JsonProperty("error-codes")]
    public IList Errors { get; set; }
}

Now in the POST or GET method in your PageModel you can call VerifyCaptcha() like this: var resultCaptcha = await VerifyCaptcha();

And then you use an if statement to check if the Captcha was successful or not. You can use the boolean Succes defined in the class CaptchaVerification. So for example if you want to have a Captcha on the page where you register an user on your website. You check the Captcha before you check the ModelState (a.k.a. the form input the user entered to register a user):

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    ReturnUrl = returnUrl;
    //Verify the Captcha
    var resultCaptcha = await VerifyCaptcha();
    //Captcha was not a succes
    if (!resultCaptcha.Success)
    {
        ModelState.AddModelError("", "Captcha is not valid");
        //Return to the register page
        return Page();
    }
    //Captcha was succesful now check the other user input (like username and password)
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = Input.Username, Email = Input.Email };
        var resultCreateUser = await _userManager.CreateAsync(user, Input.Password);
        if (resultCreateUser.Succeeded)
        {
            _logger.LogInformation("User created a new account with password.");                    
            await _signInManager.SignInAsync(user, isPersistent: false);

            //User input is wrong return to the page
            return LocalRedirect(Url.GetLocalUrl(returnUrl));
        }
        foreach (var error in resultCreateUser.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }
    //User input is wrong return to the page
    return Page();
}

And on the page itself you put this in a form:

<div class="form-group">
    <div class="g-recaptcha" data-sitekey="YOUR_PUBLIC_KEY"></div>
</div>

Replace YOUR_PUBLIC_KEY with the public key you got from Google.

Also make sure you add 'localhost' to the Google Captcha admin page as one of the Domains so that the Captcha works when testing on your local PC.

I hope this helps. If you have any questions let me know.

alexhopeoconnor commented 6 years ago

You can achieve this using the IPageFilter interface like so:

public class ValidateRecaptchaPageFilterFactory : IFilterFactory
    {
        public bool IsReusable { get => false; }

        public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<ValidateRecaptchaPageFilter>();
    }

    public class ValidateRecaptchaPageFilter : IPageFilter
    {
        #region Fields
        private readonly ILogger<ValidateRecaptchaPageFilter> _logger;
        private readonly IRecaptchaValidationService _service;
        private readonly IRecaptchaConfigurationService _configurationService;
        #endregion

        #region Methods
        public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
        {

        }

        public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
        {
            var formField = "g-recaptcha-response";
            Action invalidResponse = () => context.ModelState.AddModelError(formField, _service.ValidationMessage);

            if (_configurationService.Enabled && string.Equals("POST", context.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase))
            {
                try
                {
                    if (!context.HttpContext.Request.HasFormContentType)
                    {
                        throw new RecaptchaValidationException(string.Format(Resources.Exception_MissingFormContent, context.HttpContext.Request.ContentType), false);
                    }

                    var form = context.HttpContext.Request.ReadFormAsync().Result;
                    var response = form[formField];
                    var remoteIp = context.HttpContext.Connection?.RemoteIpAddress?.ToString();

                    if (string.IsNullOrEmpty(response))
                    {
                        invalidResponse();
                        return;
                    }

                    _service.ValidateResponseAsync(response, remoteIp).Wait();
                }
                catch (RecaptchaValidationException ex)
                {
                    _logger.LogError(ex.Message, ex);

                    if (ex.InvalidResponse)
                    {
                        invalidResponse();
                        return;
                    }
                    else
                    {
                        context.Result = new BadRequestResult();
                    }
                }
            }
        }

        public void OnPageHandlerSelected(PageHandlerSelectedContext context)
        {

        }
        #endregion

        #region Initialization
        public ValidateRecaptchaPageFilter(IRecaptchaValidationService service, IRecaptchaConfigurationService configurationService, ILoggerFactory loggerFactory)
        {
            _service = service ?? throw new ArgumentNullException(nameof(service));
            _configurationService = configurationService ?? throw new ArgumentNullException(nameof(configurationService));
            _logger = loggerFactory.CreateLogger<ValidateRecaptchaPageFilter>();
        }
        #endregion
    }

Then to apply the filter to a page the best way I could figure out how to do it from the MSDN documentation is the super clunky conventions API like this:

services.AddMvc()
                .AddRazorPagesOptions(options =>
                {
                    options.Conventions.AddPageApplicationModelConvention("/Contact", model => model.Filters.Add(new ValidateRecaptchaPageFilterFactory()));
                });
drew-fierst commented 5 years ago

Actually, you can just apply the [ValidateRecaptcha] attribute to the PageModel class and it will validate the Recaptcha for the class' OnPost() method.

[ValidateRecaptcha]
public class MyPage : PageModel {

    //this method will not have the Recaptcha validated
    public async Task<IActionResult> OnGetAsync() { . . . }

    //this method will have the Recaptcha validated
    public async Task<IActionResult> OnPostAsync() { . . . }
}

Working for me with version 1.2.1 of this package in a .NET Core 2.2 application.