diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs index 88a81fa1ec..f1efac83ce 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs @@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Localization; using Volo.Abp.ApiVersioning; +using Volo.Abp.AspNetCore.Mvc.AntiForgery; using Volo.Abp.AspNetCore.Mvc.ApiExploring; using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.AspNetCore.Mvc.DataAnnotations; @@ -94,7 +95,15 @@ namespace Volo.Abp.AspNetCore.Mvc } }); - var mvcCoreBuilder = context.Services.AddMvcCore(); + var antiForgeryPreOptions = context.Services.ExecutePreConfiguredActions(); + + var mvcCoreBuilder = context.Services.AddMvcCore(options => + { + if (antiForgeryPreOptions.AutoValidate) + { + options.Filters.Add(new AbpAutoValidateAntiforgeryTokenAttribute()); + } + }); context.Services.ExecutePreConfiguredActions(mvcCoreBuilder); var abpMvcDataAnnotationsLocalizationOptions = context.Services diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryOptions.cs index 0dc573fd5d..defc794998 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryOptions.cs @@ -9,15 +9,15 @@ public string TokenCookieName { get; set; } /// - /// Get/sets header name to transfer Anti Forgery token from client to the server. - /// Default value: "X-XSRF-TOKEN". + /// Used to find auth cookie when validating Anti Forgery token. + /// Default value: ".AspNet.ApplicationCookie". /// - public string TokenHeaderName { get; set; } + public string AuthorizationCookieName { get; set; } public AbpAntiForgeryOptions() { TokenCookieName = "XSRF-TOKEN"; - TokenHeaderName = "X-XSRF-TOKEN"; + AuthorizationCookieName = ".AspNet.ApplicationCookie"; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryPreOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryPreOptions.cs new file mode 100644 index 0000000000..887153fd1b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryPreOptions.cs @@ -0,0 +1,10 @@ +namespace Volo.Abp.AspNetCore.Mvc.AntiForgery +{ + public class AbpAntiForgeryPreOptions + { + /// + /// Default value: true. + /// + public bool AutoValidate { get; set; } = true; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAutoValidateAntiforgeryTokenAttribute.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAutoValidateAntiforgeryTokenAttribute.cs new file mode 100644 index 0000000000..346d5e9db4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAutoValidateAntiforgeryTokenAttribute.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Mvc.AntiForgery +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class AbpAutoValidateAntiforgeryTokenAttribute : Attribute, IFilterFactory, IOrderedFilter + { + /// + /// Gets the order value for determining the order of execution of filters. Filters execute in + /// ascending numeric value of the property. + /// + /// + /// + /// Filters are executed in a sequence determined by an ascending sort of the property. + /// + /// + /// The default Order for this attribute is 1000 because it must run after any filter which does authentication + /// or login in order to allow them to behave as expected (ie Unauthenticated or Redirect instead of 400). + /// + /// + /// Look at for more detailed info. + /// + /// + public int Order { get; set; } = 1000; + + /// + public bool IsReusable => true; + + /// + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + return serviceProvider.GetRequiredService(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAutoValidateAntiforgeryTokenAuthorizationFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAutoValidateAntiforgeryTokenAuthorizationFilter.cs new file mode 100644 index 0000000000..ba78746390 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAutoValidateAntiforgeryTokenAuthorizationFilter.cs @@ -0,0 +1,78 @@ +using System; +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Mvc.AntiForgery +{ + public class AbpAutoValidateAntiforgeryTokenAuthorizationFilter : AbpValidateAntiforgeryTokenAuthorizationFilter, ITransientDependency + { + private readonly AntiforgeryOptions _antiforgeryOptions; + private readonly IOptionsSnapshot _namedOptionsAccessor; + private readonly AbpAntiForgeryOptions _abpAntiForgeryOptions; + + public AbpAutoValidateAntiforgeryTokenAuthorizationFilter( + IAntiforgery antiforgery, + IOptions antiforgeryOptions, + IOptions abpAntiForgeryOptions, + IOptionsSnapshot namedOptionsAccessor, + ILogger logger) + : base(antiforgery, antiforgeryOptions, abpAntiForgeryOptions, namedOptionsAccessor, logger) + { + _namedOptionsAccessor = namedOptionsAccessor; + _abpAntiForgeryOptions = abpAntiForgeryOptions.Value; + _antiforgeryOptions = antiforgeryOptions.Value; + } + + protected override bool ShouldValidate(AuthorizationFilterContext context) + { + if (!ShouldValidateInternal(context)) + { + return false; + } + + var cookieAuthenticationOptions = _namedOptionsAccessor.Get(_abpAntiForgeryOptions.AuthorizationCookieName); + + //Always perform antiforgery validation when request contains authentication cookie + if (cookieAuthenticationOptions?.Cookie.Name != null && + context.HttpContext.Request.Cookies.ContainsKey(cookieAuthenticationOptions.Cookie.Name)) + { + return true; + } + + //No need to validate if antiforgery cookie is not sent. + //That means the request is sent from a non-browser client. + //See https://github.com/aspnet/Antiforgery/issues/115 + if (!context.HttpContext.Request.Cookies.ContainsKey(_antiforgeryOptions.Cookie.Name)) + { + return false; + } + + // Anything else requires a token. + return true; + } + + private static bool ShouldValidateInternal(AuthorizationFilterContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var method = context.HttpContext.Request.Method; + if (string.Equals("GET", method, StringComparison.OrdinalIgnoreCase) || + string.Equals("HEAD", method, StringComparison.OrdinalIgnoreCase) || + string.Equals("TRACE", method, StringComparison.OrdinalIgnoreCase) || + string.Equals("OPTIONS", method, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Anything else requires a token. + return true; + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpValidateAntiForgeryTokenAttribute.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpValidateAntiForgeryTokenAttribute.cs new file mode 100644 index 0000000000..853f642b29 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpValidateAntiForgeryTokenAttribute.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Mvc.AntiForgery +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class AbpValidateAntiForgeryTokenAttribute : Attribute, IFilterFactory, IOrderedFilter + { + /// + /// Gets the order value for determining the order of execution of filters. Filters execute in + /// ascending numeric value of the property. + /// + /// + /// + /// Filters are executed in an ordering determined by an ascending sort of the property. + /// + /// + /// The default Order for this attribute is 1000 because it must run after any filter which does authentication + /// or login in order to allow them to behave as expected (ie Unauthenticated or Redirect instead of 400). + /// + /// + /// Look at for more detailed info. + /// + /// + public int Order { get; set; } = 1000; + + /// + public bool IsReusable => true; + + /// + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + return serviceProvider.GetRequiredService(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpValidateAntiforgeryTokenAuthorizationFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpValidateAntiforgeryTokenAuthorizationFilter.cs new file mode 100644 index 0000000000..662f161595 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpValidateAntiforgeryTokenAuthorizationFilter.cs @@ -0,0 +1,100 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Volo.Abp.AspNetCore.Mvc.AntiForgery +{ + public class AbpValidateAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy + { + private IAntiforgery _antiforgery; + private readonly AntiforgeryOptions _antiforgeryOptions; + private readonly IOptionsSnapshot _namedOptionsAccessor; + private readonly AbpAntiForgeryOptions _abpAntiForgeryOptions; + private readonly ILogger _logger; + + public AbpValidateAntiforgeryTokenAuthorizationFilter( + IAntiforgery antiforgery, + IOptions antiforgeryOptions, + IOptions abpAntiForgeryOptions, + IOptionsSnapshot namedOptionsAccessor, + ILogger logger) + { + _antiforgery = antiforgery; + _antiforgeryOptions = antiforgeryOptions.Value; + _namedOptionsAccessor = namedOptionsAccessor; + _logger = logger; + _abpAntiForgeryOptions = abpAntiForgeryOptions.Value; + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.IsEffectivePolicy(this)) + { + _logger.LogInformation("Skipping the execution of current filter as its not the most effective filter implementing the policy " + typeof(IAntiforgeryPolicy)); + return; + } + + if (ShouldValidate(context)) + { + try + { + await _antiforgery.ValidateRequestAsync(context.HttpContext); + } + catch (AntiforgeryValidationException exception) + { + _logger.LogError(exception.Message, exception); + context.Result = new AntiforgeryValidationFailedResult(); + } + } + } + + protected virtual bool ShouldValidate(AuthorizationFilterContext context) + { + if (!ShouldValidateInternal(context)) + { + return false; + } + + var cookieAuthenticationOptions = _namedOptionsAccessor.Get(_abpAntiForgeryOptions.AuthorizationCookieName); + + //Always perform antiforgery validation when request contains authentication cookie + if (cookieAuthenticationOptions?.Cookie.Name != null && + context.HttpContext.Request.Cookies.ContainsKey(cookieAuthenticationOptions.Cookie.Name)) + { + return true; + } + + //No need to validate if antiforgery cookie is not sent. + //That means the request is sent from a non-browser client. + //See https://github.com/aspnet/Antiforgery/issues/115 + if (!context.HttpContext.Request.Cookies.ContainsKey(_antiforgeryOptions.Cookie.Name)) + { + return false; + } + + // Anything else requires a token. + return true; + } + + private static bool ShouldValidateInternal(AuthorizationFilterContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return true; + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs index 263d0532d0..001fc6a02d 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.DependencyInjection; using Nito.AsyncEx;