diff --git a/AspNetCore.ReCaptcha.Tests/ReCaptchaServiceTests.cs b/AspNetCore.ReCaptcha.Tests/ReCaptchaServiceTests.cs index 8fa309f..907728d 100644 --- a/AspNetCore.ReCaptcha.Tests/ReCaptchaServiceTests.cs +++ b/AspNetCore.ReCaptcha.Tests/ReCaptchaServiceTests.cs @@ -5,6 +5,8 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using Moq.Protected; @@ -14,7 +16,7 @@ namespace AspNetCore.ReCaptcha.Tests { public class ReCaptchaServiceTests { - private ReCaptchaService CreateService(HttpClient httpClient = null, Mock> reCaptchaSettingsMock = null) + private ReCaptchaService CreateService(HttpClient httpClient = null, Mock> reCaptchaSettingsMock = null, ILogger logger = null) { httpClient ??= new HttpClient(); @@ -31,7 +33,9 @@ private ReCaptchaService CreateService(HttpClient httpClient = null, Mock x.Value).Returns(reCaptchaSettings); } - return new ReCaptchaService(httpClient, reCaptchaSettingsMock.Object); + logger ??= new NullLogger(); + + return new ReCaptchaService(httpClient, reCaptchaSettingsMock.Object, logger); } [Theory] @@ -67,7 +71,57 @@ public void TestVerifyAsync(bool successResult) var result = reCaptchaService.VerifyAsync("123").Result; mockHttpMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.IsAny(), ItExpr.IsAny()); - Assert.Equal(reCaptchaResponse.Success, result); + Assert.Equal(successResult, result); + } + + [Theory] + [InlineData("missing-input-secret", LogLevel.Warning, "recaptcha verify returned error code missing-input-secret, this could indicate an invalid secretkey.")] + [InlineData("invalid-input-secret", LogLevel.Warning, "recaptcha verify returned error code invalid-input-secret, this could indicate an invalid secretkey.")] + [InlineData("missing-input-response", LogLevel.Debug, "recaptcha verify returned error code missing-input-response, this indicates the user didn't succeed the captcha.")] + [InlineData("invalid-input-response", LogLevel.Debug, "recaptcha verify returned error code invalid-input-response, this indicates the user didn't succeed the captcha.")] + [InlineData("bad-request", LogLevel.Debug, "recaptcha verify returned error code bad-request.")] + [InlineData("timeout-or-duplicate", LogLevel.Debug, "recaptcha verify returned error code timeout-or-duplicate.")] + public void TestVerifyWithErrorAsync(string errorCode, LogLevel expectedLogLevel, string expectedLogMessage) + { + var reCaptchaResponse = new ReCaptchaResponse() + { + Action = "Test", + ChallengeTimestamp = new DateTime(2022, 2, 10, 15, 14, 13), + Hostname = "Test", + Success = false, + ErrorCodes = new[] { errorCode } + }; + + var mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(JsonSerializer.Serialize(reCaptchaResponse), Encoding.UTF8,"application/json")}); + + var httpClient = new HttpClient(mockHttpMessageHandler.Object) + { + BaseAddress = new Uri("https://www.google.com/recaptcha/"), + }; + + var logger = new TestLogger(); + + var reCaptchaService = CreateService(httpClient, logger: logger); + + var result = reCaptchaService.VerifyAsync("123").Result; + + mockHttpMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.IsAny(), ItExpr.IsAny()); + Assert.False(reCaptchaResponse.Success); + Assert.False(result); + + Assert.Equal(2, logger.LogEntries.Count); + Assert.Equal(LogLevel.Trace, logger.LogEntries[0].LogLevel); + Assert.Equal(@$"recaptcha response: {{""success"":false,""score"":0,""action"":""Test"",""challenge_ts"":""2022-02-10T15:14:13"",""hostname"":""Test"",""error-codes"":[""{errorCode}""]}}", logger.LogEntries[0].Message); + + Assert.Equal(expectedLogLevel, logger.LogEntries[1].LogLevel); + Assert.Equal(expectedLogMessage, logger.LogEntries[1].Message); } } } diff --git a/AspNetCore.ReCaptcha.Tests/TestLogger.cs b/AspNetCore.ReCaptcha.Tests/TestLogger.cs new file mode 100644 index 0000000..0c95315 --- /dev/null +++ b/AspNetCore.ReCaptcha.Tests/TestLogger.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace AspNetCore.ReCaptcha.Tests; + +public class TestLogger : ILogger + where T : class +{ + public List<(LogLevel LogLevel, string Message)> LogEntries { get; } = new List<(LogLevel, string)>(); + + public IDisposable BeginScope(TState state) + { + return new NullDisposable(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + LogEntries.Add((logLevel, formatter(state, exception))); + } +} + +internal class NullDisposable : IDisposable +{ + public void Dispose() + { + } +} diff --git a/AspNetCore.ReCaptcha/AspNetCore.ReCaptcha.csproj b/AspNetCore.ReCaptcha/AspNetCore.ReCaptcha.csproj index 846ec8f..c75ab9b 100644 --- a/AspNetCore.ReCaptcha/AspNetCore.ReCaptcha.csproj +++ b/AspNetCore.ReCaptcha/AspNetCore.ReCaptcha.csproj @@ -3,7 +3,7 @@ netcoreapp3.1;net5.0;net6.0 AspNetCore.ReCaptcha - 1.5.1 + 1.5.2 Michaelvs97,sleeuwen Google ReCAPTCHA v2/v3 Library for .NET Core 3.1 and .NET 5.0/6.0 Google ReCAPTCHA v2/v3 Library for .NET Core 3.1 and .NET 5.0/6.0 diff --git a/AspNetCore.ReCaptcha/ReCaptchaHelper.cs b/AspNetCore.ReCaptcha/ReCaptchaHelper.cs index ff94075..980e6aa 100644 --- a/AspNetCore.ReCaptcha/ReCaptchaHelper.cs +++ b/AspNetCore.ReCaptcha/ReCaptchaHelper.cs @@ -14,6 +14,8 @@ public static class ReCaptchaHelper { private static IHttpClientBuilder AddReCaptchaServices(this IServiceCollection services) { + services.AddLogging(); + services.PostConfigure(settings => { settings.LocalizerProvider ??= (modelType, localizerFactory) => localizerFactory.Create(modelType); diff --git a/AspNetCore.ReCaptcha/ReCaptchaService.cs b/AspNetCore.ReCaptcha/ReCaptchaService.cs index de8a493..8ea62a5 100644 --- a/AspNetCore.ReCaptcha/ReCaptchaService.cs +++ b/AspNetCore.ReCaptcha/ReCaptchaService.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace AspNetCore.ReCaptcha @@ -9,11 +10,13 @@ namespace AspNetCore.ReCaptcha internal class ReCaptchaService : IReCaptchaService { private readonly HttpClient _client; + private readonly ILogger _logger; private readonly ReCaptchaSettings _reCaptchaSettings; - public ReCaptchaService(HttpClient client, IOptions reCaptchaSettings) + public ReCaptchaService(HttpClient client, IOptions reCaptchaSettings, ILogger logger) { _client = client; + _logger = logger; _reCaptchaSettings = reCaptchaSettings.Value; } @@ -42,9 +45,24 @@ public async Task GetVerifyResponseAsync(string reCaptchaResp var result = await _client.PostAsync("api/siteverify", body); var stringResult = await result.Content.ReadAsStringAsync(); + _logger?.LogTrace("recaptcha response: {recaptchaResponse}", stringResult); var obj = JsonSerializer.Deserialize(stringResult); + if (obj.ErrorCodes?.Length > 0 && _logger?.IsEnabled(LogLevel.Warning) == true) + { + for (var i = 0; i < obj.ErrorCodes.Length; i++) + { + var errorCode = obj.ErrorCodes[i]; + if (errorCode.EndsWith("-input-secret")) + _logger?.LogWarning("recaptcha verify returned error code {ErrorCode}, this could indicate an invalid secretkey.", errorCode); + else if (errorCode.EndsWith("-input-response")) + _logger?.LogDebug("recaptcha verify returned error code {ErrorCode}, this indicates the user didn't succeed the captcha.", errorCode); + else + _logger?.LogDebug("recaptcha verify returned error code {ErrorCode}.", errorCode); + } + } + return obj; } } diff --git a/AspNetCore.ReCaptcha/RecaptchaResponse.cs b/AspNetCore.ReCaptcha/RecaptchaResponse.cs index 75ad8ca..8459d05 100644 --- a/AspNetCore.ReCaptcha/RecaptchaResponse.cs +++ b/AspNetCore.ReCaptcha/RecaptchaResponse.cs @@ -15,5 +15,7 @@ public class ReCaptchaResponse public DateTime ChallengeTimestamp { get; set; } [JsonPropertyName("hostname")] public string Hostname { get; set; } + [JsonPropertyName("error-codes")] + public string[] ErrorCodes { get; set; } } }