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
|
// This does caching so it should stay singleton
|
||||||
services.TryAddSingleton<ITempDataProvider, SessionStateTempDataProvider>();
|
services.TryAddSingleton<ITempDataProvider, SessionStateTempDataProvider>();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Antiforgery
|
||||||
|
//
|
||||||
|
services.TryAddSingleton<ValidateAntiforgeryTokenAuthorizationFilter>();
|
||||||
|
services.TryAddSingleton<AutoValidateAntiforgeryTokenAuthorizationFilter>();
|
||||||
|
|
||||||
// These are stateless so their lifetime isn't really important.
|
// These are stateless so their lifetime isn't really important.
|
||||||
services.TryAddSingleton<ITempDataDictionaryFactory, TempDataDictionaryFactory>();
|
services.TryAddSingleton<ITempDataDictionaryFactory, TempDataDictionaryFactory>();
|
||||||
services.TryAddSingleton<SaveTempDataFilter>();
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNet.Antiforgery;
|
|
||||||
using Microsoft.AspNet.Mvc.Filters;
|
using Microsoft.AspNet.Mvc.Filters;
|
||||||
using Microsoft.AspNet.Mvc.ViewFeatures;
|
using Microsoft.AspNet.Mvc.ViewFeatures.Internal;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
|
|
@ -24,8 +23,7 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
var antiforgery = serviceProvider.GetRequiredService<IAntiforgery>();
|
return serviceProvider.GetRequiredService<ValidateAntiforgeryTokenAuthorizationFilter>();
|
||||||
return new ValidateAntiforgeryTokenAuthorizationFilter(antiforgery);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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