Smarter antiforgery
Adds the concept of an IAntiforgeryPolicy marker interface as well as the ability to overide policy with a 'closer' filter. Adds a new [IgnoreAntiforgeryToken] attribute for overriding a scoped antiforgery policy. Adds a new [AutoValidateAntiforgeryToken] attribute (good name tbd) for applying an application-wide antiforgery token. The idea is that you can configure this as a global filter if your site is acting as a pure browser-based or 1st party SPA. This new attribute only validates the token for unsafe HTTP methods, so you can apply it broadly.
This commit is contained in:
parent
0a2b6205c9
commit
a500a93dfb
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
using Microsoft.AspNet.Mvc.ViewFeatures.Internal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// An attribute that causes validation of antiforgery tokens for all unsafe HTTP methods. An antiforgery
|
||||
/// token is required for HTTP methods other than GET, HEAD, OPTIONS, and TRACE.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="AutoValidateAntiforgeryTokenAttribute"/> can be applied at as a global filter to trigger
|
||||
/// validation of antiforgery tokens by default for an application. Use
|
||||
/// <see cref="IgnoreAntiforgeryTokenAttribute"/> to suppress validation of the antiforgery token for
|
||||
/// a controller or action.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
public class AutoValidateAntiforgeryTokenAttribute : Attribute, IFilterFactory, IOrderedFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||
{
|
||||
return serviceProvider.GetRequiredService<AutoValidateAntiforgeryTokenAuthorizationFilter>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -136,6 +136,12 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
// This does caching so it should stay singleton
|
||||
services.TryAddSingleton<ITempDataProvider, SessionStateTempDataProvider>();
|
||||
|
||||
//
|
||||
// Antiforgery
|
||||
//
|
||||
services.TryAddSingleton<ValidateAntiforgeryTokenAuthorizationFilter>();
|
||||
services.TryAddSingleton<AutoValidateAntiforgeryTokenAuthorizationFilter>();
|
||||
|
||||
// These are stateless so their lifetime isn't really important.
|
||||
services.TryAddSingleton<ITempDataDictionaryFactory, TempDataDictionaryFactory>();
|
||||
services.TryAddSingleton<SaveTempDataFilter>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
using Microsoft.AspNet.Mvc.ViewFeatures;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// An attribute which skips antiforgery token validation.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
public class IgnoreAntiforgeryTokenAttribute : Attribute, IAntiforgeryPolicy, IOrderedFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int Order { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Antiforgery;
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
|
||||
{
|
||||
public class AutoValidateAntiforgeryTokenAuthorizationFilter : ValidateAntiforgeryTokenAuthorizationFilter
|
||||
{
|
||||
public AutoValidateAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery)
|
||||
: base(antiforgery)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool ShouldValidate(AuthorizationContext 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Antiforgery;
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
using Microsoft.AspNet.Mvc.Internal;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
|
||||
{
|
||||
public class ValidateAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy
|
||||
{
|
||||
private readonly IAntiforgery _antiforgery;
|
||||
|
||||
public ValidateAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery)
|
||||
{
|
||||
if (antiforgery == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(antiforgery));
|
||||
}
|
||||
|
||||
_antiforgery = antiforgery;
|
||||
}
|
||||
|
||||
public Task OnAuthorizationAsync(AuthorizationContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (IsClosestAntiforgeryPolicy(context.Filters) && ShouldValidate(context))
|
||||
{
|
||||
return _antiforgery.ValidateRequestAsync(context.HttpContext);
|
||||
}
|
||||
|
||||
return TaskCache.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual bool ShouldValidate(AuthorizationContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsClosestAntiforgeryPolicy(IList<IFilterMetadata> filters)
|
||||
{
|
||||
// Determine if this instance is the 'effective' antiforgery policy.
|
||||
for (var i = filters.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var filter = filters[i];
|
||||
if (filter is IAntiforgeryPolicy)
|
||||
{
|
||||
return object.ReferenceEquals(this, filter);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Fail("The current instance should be in the list of filters.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,8 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Antiforgery;
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
using Microsoft.AspNet.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNet.Mvc.ViewFeatures.Internal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
|
|
@ -24,8 +23,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||
{
|
||||
var antiforgery = serviceProvider.GetRequiredService<IAntiforgery>();
|
||||
return new ValidateAntiforgeryTokenAuthorizationFilter(antiforgery);
|
||||
return serviceProvider.GetRequiredService<ValidateAntiforgeryTokenAuthorizationFilter>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ViewFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// A marker interface for filters which define a policy for antiforgery token validation.
|
||||
/// </summary>
|
||||
public interface IAntiforgeryPolicy : IFilterMetadata
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Antiforgery;
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ViewFeatures
|
||||
{
|
||||
public class ValidateAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter
|
||||
{
|
||||
private readonly IAntiforgery _antiforgery;
|
||||
|
||||
public ValidateAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery)
|
||||
{
|
||||
if (antiforgery == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(antiforgery));
|
||||
}
|
||||
|
||||
_antiforgery = antiforgery;
|
||||
}
|
||||
|
||||
public Task OnAuthorizationAsync(AuthorizationContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
return _antiforgery.ValidateRequestAsync(context.HttpContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Antiforgery;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Http.Internal;
|
||||
using Microsoft.AspNet.Mvc.Abstractions;
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
|
||||
{
|
||||
public class AutoValidateAntiforgeryTokenAuthorizationFilterTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("PUT")]
|
||||
[InlineData("POsT")]
|
||||
[InlineData("DeLETE")]
|
||||
public async Task Filter_ValidatesAntiforgery_ForUnsafeMethod(string httpMethod)
|
||||
{
|
||||
// Arrange
|
||||
var antiforgery = new Mock<IAntiforgery>(MockBehavior.Strict);
|
||||
antiforgery
|
||||
.Setup(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()))
|
||||
.Returns(Task.FromResult(0))
|
||||
.Verifiable();
|
||||
|
||||
var filter = new AutoValidateAntiforgeryTokenAuthorizationFilter(antiforgery.Object);
|
||||
|
||||
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||
actionContext.HttpContext.Request.Method = httpMethod;
|
||||
|
||||
var context = new AuthorizationContext(actionContext, new[] { filter });
|
||||
|
||||
// Act
|
||||
await filter.OnAuthorizationAsync(context);
|
||||
|
||||
// Assert
|
||||
antiforgery.Verify();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("GET")]
|
||||
[InlineData("HEAD")]
|
||||
[InlineData("TracE")]
|
||||
[InlineData("OPTIONs")]
|
||||
public async Task Filter_SkipsAntiforgeryVerification_ForSafeMethod(string httpMethod)
|
||||
{
|
||||
// Arrange
|
||||
var antiforgery = new Mock<IAntiforgery>(MockBehavior.Strict);
|
||||
antiforgery
|
||||
.Setup(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()))
|
||||
.Returns(Task.FromResult(0))
|
||||
.Verifiable();
|
||||
|
||||
var filter = new AutoValidateAntiforgeryTokenAuthorizationFilter(antiforgery.Object);
|
||||
|
||||
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||
actionContext.HttpContext.Request.Method = httpMethod;
|
||||
|
||||
var context = new AuthorizationContext(actionContext, new[] { filter });
|
||||
|
||||
// Act
|
||||
await filter.OnAuthorizationAsync(context);
|
||||
|
||||
// Assert
|
||||
antiforgery.Verify(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()), Times.Never());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Filter_SkipsAntiforgeryVerification_WhenOverridden()
|
||||
{
|
||||
// Arrange
|
||||
var antiforgery = new Mock<IAntiforgery>(MockBehavior.Strict);
|
||||
antiforgery
|
||||
.Setup(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()))
|
||||
.Returns(Task.FromResult(0))
|
||||
.Verifiable();
|
||||
|
||||
var filter = new AutoValidateAntiforgeryTokenAuthorizationFilter(antiforgery.Object);
|
||||
|
||||
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||
actionContext.HttpContext.Request.Method = "POST";
|
||||
|
||||
var context = new AuthorizationContext(actionContext, new IFilterMetadata[]
|
||||
{
|
||||
filter,
|
||||
new IgnoreAntiforgeryTokenAttribute(),
|
||||
});
|
||||
|
||||
// Act
|
||||
await filter.OnAuthorizationAsync(context);
|
||||
|
||||
// Assert
|
||||
antiforgery.Verify(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()), Times.Never());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Antiforgery;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Http.Internal;
|
||||
using Microsoft.AspNet.Mvc.Abstractions;
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
|
||||
{
|
||||
public class ValidateAntiforgeryTokenAuthorizationFilterTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("PUT")]
|
||||
[InlineData("POsT")]
|
||||
[InlineData("DeLETE")]
|
||||
[InlineData("GET")]
|
||||
[InlineData("HEAD")]
|
||||
[InlineData("TracE")]
|
||||
[InlineData("OPTIONs")]
|
||||
public async Task Filter_ValidatesAntiforgery_ForAllMethods(string httpMethod)
|
||||
{
|
||||
// Arrange
|
||||
var antiforgery = new Mock<IAntiforgery>(MockBehavior.Strict);
|
||||
antiforgery
|
||||
.Setup(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()))
|
||||
.Returns(Task.FromResult(0))
|
||||
.Verifiable();
|
||||
|
||||
var filter = new ValidateAntiforgeryTokenAuthorizationFilter(antiforgery.Object);
|
||||
|
||||
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||
actionContext.HttpContext.Request.Method = httpMethod;
|
||||
|
||||
var context = new AuthorizationContext(actionContext, new[] { filter });
|
||||
|
||||
// Act
|
||||
await filter.OnAuthorizationAsync(context);
|
||||
|
||||
// Assert
|
||||
antiforgery.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Filter_SkipsAntiforgeryVerification_WhenOverridden()
|
||||
{
|
||||
// Arrange
|
||||
var antiforgery = new Mock<IAntiforgery>(MockBehavior.Strict);
|
||||
antiforgery
|
||||
.Setup(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()))
|
||||
.Returns(Task.FromResult(0))
|
||||
.Verifiable();
|
||||
|
||||
var filter = new ValidateAntiforgeryTokenAuthorizationFilter(antiforgery.Object);
|
||||
|
||||
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||
actionContext.HttpContext.Request.Method = "POST";
|
||||
|
||||
var context = new AuthorizationContext(actionContext, new IFilterMetadata[]
|
||||
{
|
||||
filter,
|
||||
new IgnoreAntiforgeryTokenAttribute(),
|
||||
});
|
||||
|
||||
// Act
|
||||
await filter.OnAuthorizationAsync(context);
|
||||
|
||||
// Assert
|
||||
antiforgery.Verify(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()), Times.Never());
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue