diff --git a/sample/TestWebApp/Program.cs b/sample/TestWebApp/Program.cs index 278c997..164c790 100644 --- a/sample/TestWebApp/Program.cs +++ b/sample/TestWebApp/Program.cs @@ -1,20 +1,19 @@ using System.IO; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; namespace TestWebApp { - public class Program + public static class Program { public static void Main(string[] args) { - var host = new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) .UseStartup() .Build(); - - host.Run(); - } } } diff --git a/sample/TestWebApp/Startup.cs b/sample/TestWebApp/Startup.cs index 8dfcb8c..e6509fc 100644 --- a/sample/TestWebApp/Startup.cs +++ b/sample/TestWebApp/Startup.cs @@ -1,5 +1,7 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; +#undef USE_NONCE + +using Joonasw.AspNetCore.SecurityHeaders; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -9,18 +11,12 @@ namespace TestWebApp { public class Startup { - - public Startup(IHostingEnvironment env) + public Startup(IConfiguration configuration) { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddUserSecrets(); - - Configuration = builder.Build(); + Configuration = configuration; } - public IConfigurationRoot Configuration { get; } + public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 @@ -34,6 +30,12 @@ public void ConfigureServices(IServiceCollection services) SecretKey = Configuration["Recaptcha:SecretKey"], ValidationMessage = "Are you a robot?" }); + +#if (USE_NONCE) + { + services.AddCsp(nonceByteAmount: 32); + } +#endif } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -44,6 +46,14 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) app.UseDeveloperExceptionPage(); +#if (USE_NONCE) + app.UseCsp(csp => + { + csp.AllowScripts.AddNonce(); + csp.SetReportOnly(); + }); +#endif + app.UseStaticFiles(); app.UseMvc(routes => diff --git a/sample/TestWebApp/TestWebApp.csproj b/sample/TestWebApp/TestWebApp.csproj index 42d0854..adc555b 100644 --- a/sample/TestWebApp/TestWebApp.csproj +++ b/sample/TestWebApp/TestWebApp.csproj @@ -1,50 +1,41 @@ - - + + - netcoreapp1.0 + netcoreapp2.1 true TestWebApp - Exe TestWebApp TestWebApp-45EC5266-9512-4AF0-9BA1-F9C3DC7D57D0 - 1.0.4 - $(PackageTargetFallback);dotnet5.6;dnxcore50;portable-net45+win8 - - + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + + PreserveNewest - - - - true - - + + - - - + + + + + + + true + recaptcha-testweb-d2ab3bfe-f40f-4885-850b-9a303a9398ea + + - - - - - - - - - - - - + + + - - + + - - - - - + + + + diff --git a/src/PaulMiami.AspNetCore.Mvc.Recaptcha/PaulMiami.AspNetCore.Mvc.Recaptcha.csproj b/src/PaulMiami.AspNetCore.Mvc.Recaptcha/PaulMiami.AspNetCore.Mvc.Recaptcha.csproj index 9580c57..12720cd 100644 --- a/src/PaulMiami.AspNetCore.Mvc.Recaptcha/PaulMiami.AspNetCore.Mvc.Recaptcha.csproj +++ b/src/PaulMiami.AspNetCore.Mvc.Recaptcha/PaulMiami.AspNetCore.Mvc.Recaptcha.csproj @@ -1,4 +1,4 @@ - + This is a helper library for google reCAPTCHA 2.0 @@ -11,7 +11,7 @@ https://github.com/PaulMiami/reCAPTCHA/wiki/Change-log reCAPTCHA 2.0 for ASPNET Core 1.2.1 Paul Biccherai - netstandard1.6;net451 + netstandard2.0;net461 true PaulMiami.AspNetCore.Mvc.Recaptcha PaulMiami.AspNetCore.Mvc.Recaptcha @@ -20,7 +20,7 @@ https://github.com/PaulMiami/reCAPTCHA/wiki/Change-log https://raw.githubusercontent.com/PaulMiami/reCAPTCHA/master/LICENSE git https://github.com/PaulMiami/reCAPTCHA - 1.6.0 + 2.0.0 false false false @@ -32,15 +32,16 @@ https://github.com/PaulMiami/reCAPTCHA/wiki/Change-log - - - - - - + + + + + + + - + diff --git a/src/PaulMiami.AspNetCore.Mvc.Recaptcha/TagHelpers/RecaptchaScriptTagHelper.cs b/src/PaulMiami.AspNetCore.Mvc.Recaptcha/TagHelpers/RecaptchaScriptTagHelper.cs index c537b10..35f2c3d 100644 --- a/src/PaulMiami.AspNetCore.Mvc.Recaptcha/TagHelpers/RecaptchaScriptTagHelper.cs +++ b/src/PaulMiami.AspNetCore.Mvc.Recaptcha/TagHelpers/RecaptchaScriptTagHelper.cs @@ -8,13 +8,15 @@ using Microsoft.AspNetCore.Razor.TagHelpers; using System; using Microsoft.AspNetCore.Localization; +using Joonasw.AspNetCore.SecurityHeaders.Csp; namespace PaulMiami.AspNetCore.Mvc.Recaptcha.TagHelpers { public class RecaptchaScriptTagHelper : TagHelper { - private IRecaptchaConfigurationService _service; - private IHttpContextAccessor _contextAccessor; + private readonly ICspNonceService _nonceService; + private readonly IRecaptchaConfigurationService _service; + private readonly IHttpContextAccessor _contextAccessor; private const string _scriptSnippet = "var {0}=function(e){{var r=$('#{1}');r.length&&r.hide()}};$.validator.setDefaults({{submitHandler:function(){{var e=this,r=''!==grecaptcha.getResponse(),a='{2}',t=$('#{1}', e.currentForm);if(t.length===0)return !0;return a&&(r?t.length&&t.hide():(e.errorList.push({{message:a}}),$(e.currentForm).triggerHandler('invalid-form',[e]),t.length&&(t.html(a),t.show()))),r}}}});"; @@ -28,6 +30,15 @@ public RecaptchaScriptTagHelper(IRecaptchaConfigurationService service, IHttpCon _service = service; _contextAccessor = contextAccessor; + var services = contextAccessor.HttpContext.RequestServices; + if (!(services is null)) + { + var nonceService = services.GetService(typeof(ICspNonceService)); + if (!(nonceService is null)) + { + _nonceService = nonceService as CspNonceService; + } + } } [HtmlAttributeName(JqueryValidationAttributeName)] @@ -61,6 +72,12 @@ public override void Process(TagHelperContext context, TagHelperOutput output) if (JqueryValidation ?? true) { var script = new TagBuilder("script"); + + if (!(_nonceService is null)) + { + script.Attributes.Add("nonce", _nonceService.GetNonce()); + } + script.TagRenderMode = TagRenderMode.Normal; script.InnerHtml.AppendHtml(string.Format(_scriptSnippet, RecaptchaTagHelper.RecaptchaValidationJSCallBack, ValidationMessageElementId, _service.ValidationMessage)); diff --git a/src/PaulMiami.AspNetCore.Mvc.Recaptcha/TagHelpers/RecaptchaTagHelper.cs b/src/PaulMiami.AspNetCore.Mvc.Recaptcha/TagHelpers/RecaptchaTagHelper.cs index 7e7d2e4..faca112 100644 --- a/src/PaulMiami.AspNetCore.Mvc.Recaptcha/TagHelpers/RecaptchaTagHelper.cs +++ b/src/PaulMiami.AspNetCore.Mvc.Recaptcha/TagHelpers/RecaptchaTagHelper.cs @@ -10,7 +10,7 @@ namespace PaulMiami.AspNetCore.Mvc.Recaptcha.TagHelpers { public class RecaptchaTagHelper : TagHelper { - private IRecaptchaConfigurationService _service; + private readonly IRecaptchaConfigurationService _service; internal const string RecaptchaValidationJSCallBack = "recaptchaValidated"; diff --git a/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test.csproj b/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test.csproj index 66dfc2b..3b978a7 100644 --- a/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test.csproj +++ b/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test.csproj @@ -1,57 +1,54 @@ - - + + - netcoreapp1.0 + netcoreapp2.1 true PaulMiami.AspNetCore.Mvc.Recaptcha.Test PaulMiami.AspNetCore.Mvc.Recaptcha.Test true - 1.0.4 - $(PackageTargetFallback);dotnet5.6;dnxcore50;portable-net45+win8 + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; false false false - - + + - - + + - - - - - - + + + + - - - + + + - - - - - - - - - + --> + + + full + True + + + + + + + + + + + + + + + + diff --git a/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/TestLogger.cs b/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/TestLogger.cs new file mode 100644 index 0000000..0472117 --- /dev/null +++ b/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/TestLogger.cs @@ -0,0 +1,33 @@ +namespace PaulMiami.AspNetCore.Mvc.Recaptcha.Test +{ + using System; + using System.Collections.Generic; + using Microsoft.Extensions.Logging; + + public class TestLogger : ILogger, IDisposable + { + public List Log { get; } = new List(); + public int ScopeCount { get; set; } = 0; + + public void Dispose() + { + // Method intentionally left empty. + } + + IDisposable ILogger.BeginScope(TState state) + { + ScopeCount++; + return this; + } + + bool ILogger.IsEnabled(LogLevel logLevel) + { + return true; + } + + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Log.Add(formatter(state, exception)); + } + } +} diff --git a/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/TestLoggerFactory.cs b/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/TestLoggerFactory.cs new file mode 100644 index 0000000..314d842 --- /dev/null +++ b/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/TestLoggerFactory.cs @@ -0,0 +1,43 @@ +namespace PaulMiami.AspNetCore.Mvc.Recaptcha.Test +{ + using Microsoft.Extensions.Logging; + + public class TestLoggerFactory : ILoggerFactory + { + private readonly ILogger _logger; + + public TestLoggerFactory(ILogger logger) + { + _logger = logger; + } + + void ILoggerFactory.AddProvider(ILoggerProvider provider) + { + throw new System.NotImplementedException(); + } + + ILogger ILoggerFactory.CreateLogger(string categoryName) + { + return _logger; + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + void System.IDisposable.Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + #endregion + } +} diff --git a/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/ValidateRecaptchaFilterTest.cs b/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/ValidateRecaptchaFilterTest.cs index 45b1df6..598e8ce 100644 --- a/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/ValidateRecaptchaFilterTest.cs +++ b/test/PaulMiami.AspNetCore.Mvc.Recaptcha.Test/ValidateRecaptchaFilterTest.cs @@ -11,12 +11,13 @@ using Moq; using System.Threading.Tasks; using Xunit; -using Microsoft.Extensions.Logging.Testing; using System.Collections.Generic; using Microsoft.Extensions.Primitives; using System; using System.Net; using System.Linq; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging; namespace PaulMiami.AspNetCore.Mvc.Recaptcha.Test { @@ -131,8 +132,8 @@ public async Task TestPostFail(string httpMethod) .Returns(true) .Verifiable(); - var sink = new TestSink(); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var logger = new TestLogger(); + var loggerFactory = new TestLoggerFactory(logger); var filter = new ValidateRecaptchaFilter(recaptchaService.Object, configurationService.Object, loggerFactory); @@ -153,9 +154,9 @@ public async Task TestPostFail(string httpMethod) recaptchaService.Verify(); - Assert.Empty(sink.Scopes); - Assert.Single(sink.Writes); - Assert.Equal($"Recaptcha validation failed. {errorMessage}", sink.Writes[0].State?.ToString()); + Assert.Equal(0, logger.ScopeCount); + Assert.Single(logger.Log); + Assert.Equal($"Recaptcha validation failed. {errorMessage}", logger.Log[0]); var httpBadRequest = Assert.IsType(context.Result); Assert.Equal(StatusCodes.Status400BadRequest, httpBadRequest.StatusCode); Assert.True(context.ModelState.IsValid); @@ -190,8 +191,8 @@ public async Task TestPostFailInvalidResponse(string httpMethod) .Returns(true) .Verifiable(); - var sink = new TestSink(); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var logger = new TestLogger(); + var loggerFactory = new TestLoggerFactory(logger); var filter = new ValidateRecaptchaFilter(recaptchaService.Object, configurationService.Object, loggerFactory); @@ -212,14 +213,14 @@ public async Task TestPostFailInvalidResponse(string httpMethod) recaptchaService.Verify(); - Assert.Empty(sink.Scopes); - Assert.Single(sink.Writes); - Assert.Equal($"Recaptcha validation failed. {errorMessage}", sink.Writes[0].State?.ToString()); + Assert.Equal(0, logger.ScopeCount); + Assert.Single(logger.Log); + Assert.Equal($"Recaptcha validation failed. {errorMessage}", logger.Log[0]); Assert.Null(context.Result); Assert.False(context.ModelState.IsValid); Assert.NotEmpty(context.ModelState); Assert.NotNull(context.ModelState["g-recaptcha-response"]); - Assert.Equal(1, context.ModelState["g-recaptcha-response"].Errors.Count); + Assert.Single(context.ModelState["g-recaptcha-response"].Errors); Assert.Equal(validationMessage, context.ModelState["g-recaptcha-response"].Errors.First().ErrorMessage); } @@ -239,8 +240,8 @@ public async Task TestPostWrongContentType(string httpMethod) .Returns(true) .Verifiable(); - var sink = new TestSink(); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var logger = new TestLogger(); + var loggerFactory = new TestLoggerFactory(logger); var filter = new ValidateRecaptchaFilter(recaptchaService.Object, configurationService.Object, loggerFactory); @@ -256,9 +257,9 @@ public async Task TestPostWrongContentType(string httpMethod) await filter.OnAuthorizationAsync(context); - Assert.Empty(sink.Scopes); - Assert.Single(sink.Writes); - Assert.Equal($"Recaptcha validation failed. The content type is 'Wrong content type', it should be form content.", sink.Writes[0].State?.ToString()); + Assert.Equal(0, logger.ScopeCount); + Assert.Single(logger.Log); + Assert.Equal($"Recaptcha validation failed. The content type is 'Wrong content type', it should be form content.", logger.Log[0]); var httpBadRequest = Assert.IsType(context.Result); Assert.Equal(StatusCodes.Status400BadRequest, httpBadRequest.StatusCode); Assert.True(context.ModelState.IsValid); @@ -272,7 +273,6 @@ public async Task TestPostFailMissingResponse(string httpMethod) { var recaptchaResponse = string.Empty; var ipAddress = new IPAddress(new byte[] { 127, 0, 0, 1 }); - var errorMessage = Guid.NewGuid().ToString(); var validationMessage = Guid.NewGuid().ToString(); var recaptchaService = new Mock(MockBehavior.Strict); @@ -315,7 +315,7 @@ public async Task TestPostFailMissingResponse(string httpMethod) Assert.False(context.ModelState.IsValid); Assert.NotEmpty(context.ModelState); Assert.NotNull(context.ModelState["g-recaptcha-response"]); - Assert.Equal(1, context.ModelState["g-recaptcha-response"].Errors.Count); + Assert.Single(context.ModelState["g-recaptcha-response"].Errors); Assert.Equal(validationMessage, context.ModelState["g-recaptcha-response"].Errors.First().ErrorMessage); } diff --git a/test/WebSites/TestSite/TestSite.csproj b/test/WebSites/TestSite/TestSite.csproj index 0f66d91..ffd2fd9 100644 --- a/test/WebSites/TestSite/TestSite.csproj +++ b/test/WebSites/TestSite/TestSite.csproj @@ -1,31 +1,26 @@ - - + + - netcoreapp1.0 + netcoreapp2.1 true TestSite Exe TestSite - 1.0.4 - $(PackageTargetFallback);dotnet5.6;portable-net45+win8 - - + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + + PreserveNewest - - + + - - + + - - - - - - - - + + + +