Skip to content

Commit 5e7e71b

Browse files
committed
Antiforgery middleware
1 parent 0de9d30 commit 5e7e71b

17 files changed

+315
-19
lines changed

Diff for: src/Antiforgery/src/AntiforgeryMiddleware.cs

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Http.Abstractions.Metadata;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.Extensions.Logging;
8+
9+
namespace Microsoft.AspNetCore.Antiforgery;
10+
11+
internal sealed partial class AntiforgeryMiddleware
12+
{
13+
private readonly IAntiforgery _antiforgery;
14+
private readonly RequestDelegate _next;
15+
private readonly ILogger<AntiforgeryMiddleware> _logger;
16+
17+
public AntiforgeryMiddleware(IAntiforgery antiforgery, RequestDelegate next, ILogger<AntiforgeryMiddleware> logger)
18+
{
19+
_antiforgery = antiforgery;
20+
_next = next;
21+
_logger = logger;
22+
}
23+
24+
public Task Invoke(HttpContext context)
25+
{
26+
var endpoint = context.GetEndpoint();
27+
if (endpoint is null)
28+
{
29+
return _next(context);
30+
}
31+
32+
var antiforgeryMetadata = endpoint.Metadata.GetMetadata<IAntiforgeryMetadata>();
33+
if (antiforgeryMetadata is null)
34+
{
35+
Log.NoAntiforgeryMetadataFound(_logger);
36+
return _next(context);
37+
}
38+
39+
if (antiforgeryMetadata is not IValidateAntiforgeryMetadata validateAntiforgeryMetadata)
40+
{
41+
Log.IgnoreAntiforgeryMetadataFound(_logger);
42+
return _next(context);
43+
}
44+
45+
if (_antiforgery is DefaultAntiforgery defaultAntiforgery)
46+
{
47+
var valueTask = defaultAntiforgery.TryValidateAsync(context, validateAntiforgeryMetadata.ValidateIdempotentRequests);
48+
if (valueTask.IsCompletedSuccessfully)
49+
{
50+
var (success, message) = valueTask.GetAwaiter().GetResult();
51+
if (success)
52+
{
53+
Log.AntiforgeryValidationSucceeded(_logger);
54+
return _next(context);
55+
}
56+
else
57+
{
58+
Log.AntiforgeryValidationFailed(_logger, message);
59+
return WriteAntiforgeryInvalidResponseAsync(context, message);
60+
}
61+
}
62+
63+
return TryValidateAsyncAwaited(context, valueTask);
64+
}
65+
else
66+
{
67+
return ValidateNonDefaultAntiforgery(context);
68+
}
69+
}
70+
71+
private async Task TryValidateAsyncAwaited(HttpContext context, ValueTask<(bool success, string? message)> tryValidateTask)
72+
{
73+
var (success, message) = await tryValidateTask;
74+
if (success)
75+
{
76+
Log.AntiforgeryValidationSucceeded(_logger);
77+
await _next(context);
78+
}
79+
else
80+
{
81+
Log.AntiforgeryValidationFailed(_logger, message);
82+
await context.Response.WriteAsJsonAsync(new ProblemDetails
83+
{
84+
Status = StatusCodes.Status400BadRequest,
85+
Title = "Antiforgery validation failed",
86+
Detail = message,
87+
});
88+
}
89+
}
90+
91+
private async Task ValidateNonDefaultAntiforgery(HttpContext context)
92+
{
93+
if (await _antiforgery.IsRequestValidAsync(context))
94+
{
95+
Log.AntiforgeryValidationSucceeded(_logger);
96+
await _next(context);
97+
}
98+
else
99+
{
100+
Log.AntiforgeryValidationFailed(_logger, message: null);
101+
await WriteAntiforgeryInvalidResponseAsync(context, message: null);
102+
}
103+
}
104+
105+
private static Task WriteAntiforgeryInvalidResponseAsync(HttpContext context, string? message)
106+
{
107+
context.Response.StatusCode = StatusCodes.Status400BadRequest;
108+
return context.Response.WriteAsJsonAsync(new ProblemDetails
109+
{
110+
Status = StatusCodes.Status400BadRequest,
111+
Title = "Antiforgery validation failed",
112+
Detail = message,
113+
});
114+
}
115+
116+
private static partial class Log
117+
{
118+
[LoggerMessage(1, LogLevel.Debug, "No antiforgery metadata found on the endpoint.", EventName = "NoAntiforgeryMetadataFound")]
119+
public static partial void NoAntiforgeryMetadataFound(ILogger logger);
120+
121+
[LoggerMessage(2, LogLevel.Debug, $"Antiforgery validation suppressed on endpoint because {nameof(IValidateAntiforgeryMetadata)} was not found.", EventName = "IgnoreAntiforgeryMetadataFound")]
122+
public static partial void IgnoreAntiforgeryMetadataFound(ILogger logger);
123+
124+
[LoggerMessage(3, LogLevel.Debug, "Antiforgery validation completed successfully.", EventName = "AntiforgeryValidationSucceeded")]
125+
public static partial void AntiforgeryValidationSucceeded(ILogger logger);
126+
127+
[LoggerMessage(4, LogLevel.Debug, "Antiforgery validation failed with message '{message}'.", EventName = "AntiforgeryValidationFailed")]
128+
public static partial void AntiforgeryValidationFailed(ILogger logger, string? message);
129+
}
130+
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Antiforgery;
6+
using Microsoft.AspNetCore.Cors.Infrastructure;
7+
8+
namespace Microsoft.AspNetCore.Builder;
9+
10+
/// <summary>
11+
/// The <see cref="IApplicationBuilder"/> extensions for adding Antiforgery middleware support.
12+
/// </summary>
13+
public static class AntiforgeryMiddlewareExtensions
14+
{
15+
/// <summary>
16+
/// Adds the Antiforgery middleware to the middleware pipeline.
17+
/// </summary>
18+
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
19+
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
20+
public static IApplicationBuilder UseAntiforgery(this IApplicationBuilder app)
21+
=> app.UseMiddleware<AntiforgeryMiddleware>();
22+
}

Diff for: src/Antiforgery/src/Internal/DefaultAntiforgery.cs

+15-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
54
using System.Diagnostics;
65
using System.Diagnostics.CodeAnalysis;
7-
using System.Threading.Tasks;
86
using Microsoft.AspNetCore.Http;
97
using Microsoft.Extensions.Logging;
108
using Microsoft.Extensions.Options;
@@ -100,35 +98,43 @@ public async Task<bool> IsRequestValidAsync(HttpContext httpContext)
10098
throw new ArgumentNullException(nameof(httpContext));
10199
}
102100

101+
var (result, _) = await TryValidateAsync(httpContext, validateIdempotentRequests: false);
102+
return result;
103+
}
104+
105+
internal async ValueTask<(bool success, string? errorMessage)> TryValidateAsync(HttpContext httpContext, bool validateIdempotentRequests)
106+
{
103107
CheckSSLConfig(httpContext);
104108

105109
var method = httpContext.Request.Method;
106-
if (HttpMethods.IsGet(method) ||
110+
if (
111+
!validateIdempotentRequests &&
112+
(HttpMethods.IsGet(method) ||
107113
HttpMethods.IsHead(method) ||
108114
HttpMethods.IsOptions(method) ||
109-
HttpMethods.IsTrace(method))
115+
HttpMethods.IsTrace(method)))
110116
{
111117
// Validation not needed for these request types.
112-
return true;
118+
return (true, null);
113119
}
114120

115121
var tokens = await _tokenStore.GetRequestTokensAsync(httpContext);
116122
if (tokens.CookieToken == null)
117123
{
118124
_logger.MissingCookieToken(_options.Cookie.Name);
119-
return false;
125+
return (false, "Missing cookie token");
120126
}
121127

122128
if (tokens.RequestToken == null)
123129
{
124130
_logger.MissingRequestToken(_options.FormFieldName, _options.HeaderName);
125-
return false;
131+
return (false, "Antiforgery token could not be found in the HTTP request.");
126132
}
127133

128134
// Extract cookie & request tokens
129135
if (!TryDeserializeTokens(httpContext, tokens, out var deserializedCookieToken, out var deserializedRequestToken))
130136
{
131-
return false;
137+
return (false, "Unable to deserialize antiforgery tokens");
132138
}
133139

134140
// Validate
@@ -147,7 +153,7 @@ public async Task<bool> IsRequestValidAsync(HttpContext httpContext)
147153
_logger.ValidationFailed(message!);
148154
}
149155

150-
return result;
156+
return (result, message);
151157
}
152158

153159
/// <inheritdoc />

Diff for: src/Antiforgery/src/PublicAPI.Unshipped.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Builder.AntiforgeryMiddlewareExtensions
3+
static Microsoft.AspNetCore.Builder.AntiforgeryMiddlewareExtensions.UseAntiforgery(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Http.Abstractions.Metadata;
5+
6+
/// <summary>
7+
/// A marker interface which can be used to identify Antiforgery metadata.
8+
/// </summary>
9+
public interface IAntiforgeryMetadata
10+
{
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Http.Abstractions.Metadata;
5+
6+
/// <summary>
7+
/// A marker interface which can be used to identify a resource with Antiforgery validation enabled.
8+
/// </summary>
9+
public interface IValidateAntiforgeryMetadata : IAntiforgeryMetadata
10+
{
11+
/// <summary>
12+
/// Gets a value that determines if idempotent HTTP methods (<c>GET</c>, <c>HEAD</c>, <c>OPTIONS</c> and <c>TRACE</c>) are validated.
13+
/// </summary>
14+
bool ValidateIdempotentRequests { get; }
15+
}
+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
#nullable enable
22
*REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string!
3+
Microsoft.AspNetCore.Http.Abstractions.Metadata.IAntiforgeryMetadata
4+
Microsoft.AspNetCore.Http.Abstractions.Metadata.IValidateAntiforgeryMetadata
5+
Microsoft.AspNetCore.Http.Abstractions.Metadata.IValidateAntiforgeryMetadata.ValidateIdempotentRequests.get -> bool
36
abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Linq;
5+
using Microsoft.AspNetCore.Http.Abstractions.Metadata;
6+
using Microsoft.AspNetCore.Mvc.Filters;
7+
using Microsoft.Extensions.Options;
8+
9+
namespace Microsoft.AspNetCore.Mvc.ApplicationModels;
10+
11+
/// <summary>
12+
/// An <see cref="IApplicationModelProvider"/> that removes antiforgery filters that appears as endpoint metadata.
13+
/// </summary>
14+
internal sealed class AntiforgeryApplicationModelProvider : IApplicationModelProvider
15+
{
16+
private readonly MvcOptions _mvcOptions;
17+
18+
public AntiforgeryApplicationModelProvider(IOptions<MvcOptions> mvcOptions)
19+
{
20+
_mvcOptions = mvcOptions.Value;
21+
}
22+
23+
// Run late in the pipeline so that we can pick up user configured AntiforgeryTokens.
24+
public int Order { get; } = 1000;
25+
26+
public void OnProvidersExecuted(ApplicationModelProviderContext context)
27+
{
28+
}
29+
30+
public void OnProvidersExecuting(ApplicationModelProviderContext context)
31+
{
32+
if (!_mvcOptions.EnableEndpointRouting)
33+
{
34+
return;
35+
}
36+
37+
foreach (var controller in context.Result.Controllers)
38+
{
39+
RemoveAntiforgeryFilters(controller.Filters, controller.Selectors);
40+
41+
foreach (var action in controller.Actions)
42+
{
43+
RemoveAntiforgeryFilters(action.Filters, action.Selectors);
44+
}
45+
}
46+
}
47+
48+
private static void RemoveAntiforgeryFilters(IList<IFilterMetadata> filters, IList<SelectorModel> selectorModels)
49+
{
50+
for (var i = filters.Count - 1; i >= 0; i--)
51+
{
52+
if (filters[i] is IAntiforgeryMetadata antiforgeryMetadata &&
53+
selectorModels.All(s => s.EndpointMetadata.Contains(antiforgeryMetadata)))
54+
{
55+
filters.RemoveAt(i);
56+
}
57+
}
58+
}
59+
}

Diff for: src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ internal static void AddMvcCoreServices(IServiceCollection services)
160160
services.TryAddSingleton<ApplicationModelFactory>();
161161
services.TryAddEnumerable(
162162
ServiceDescriptor.Transient<IApplicationModelProvider, DefaultApplicationModelProvider>());
163+
services.TryAddEnumerable(
164+
ServiceDescriptor.Transient<IApplicationModelProvider, AntiforgeryApplicationModelProvider>());
163165
services.TryAddEnumerable(
164166
ServiceDescriptor.Transient<IApplicationModelProvider, ApiBehaviorApplicationModelProvider>());
165167
services.TryAddEnumerable(

Diff for: src/Mvc/Mvc.ViewFeatures/src/AutoValidateAntiforgeryTokenAttribute.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#nullable enable
55

66
using System;
7+
using Microsoft.AspNetCore.Http.Abstractions.Metadata;
78
using Microsoft.AspNetCore.Mvc.Filters;
89
using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
910
using Microsoft.Extensions.DependencyInjection;
@@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc;
2122
/// a controller or action.
2223
/// </remarks>
2324
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
24-
public class AutoValidateAntiforgeryTokenAttribute : Attribute, IFilterFactory, IOrderedFilter
25+
public class AutoValidateAntiforgeryTokenAttribute : Attribute, IFilterFactory, IOrderedFilter, IValidateAntiforgeryMetadata
2526
{
2627
/// <summary>
2728
/// Gets the order value for determining the order of execution of filters. Filters execute in
@@ -44,6 +45,8 @@ public class AutoValidateAntiforgeryTokenAttribute : Attribute, IFilterFactory,
4445
/// <inheritdoc />
4546
public bool IsReusable => true;
4647

48+
bool IValidateAntiforgeryMetadata.ValidateIdempotentRequests => false;
49+
4750
/// <inheritdoc />
4851
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
4952
{

Diff for: src/Mvc/Mvc.ViewFeatures/src/Filters/AutoValidateAntiforgeryTokenAuthorizationFilter.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
using Microsoft.AspNetCore.Http;
77
using Microsoft.AspNetCore.Mvc.Filters;
88
using Microsoft.Extensions.Logging;
9+
using Microsoft.Extensions.Options;
910

1011
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
1112

1213
internal class AutoValidateAntiforgeryTokenAuthorizationFilter : ValidateAntiforgeryTokenAuthorizationFilter
1314
{
14-
public AutoValidateAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery, ILoggerFactory loggerFactory)
15-
: base(antiforgery, loggerFactory)
15+
public AutoValidateAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery, ILoggerFactory loggerFactory, IOptions<MvcOptions> mvcOptions)
16+
: base(antiforgery, loggerFactory, mvcOptions)
1617
{
1718
}
1819

0 commit comments

Comments
 (0)