From 4629148519cde61200c3871d9ab943607aef3e5d Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 18 Feb 2016 12:08:53 -0800 Subject: [PATCH] [Design] Add antiforgery middleware This new middleware participates in authentication and acts as a filter when the request doesn't include a valid CSRF token for a POST. Any authentication middleware that you want to validate an antiforgery token should go ahead of this middleware in the pipeline (Cookies, IISIntegration). This also takes care of automatic auth (Windows) done by weblistener. Any authentication middleware that you want to ignore antiforgery should go after this middleware in the pipeline. To facilitate this, there are a few changes in the antiforgery API surface. Namely we can now pass in a principal to validate tokens. You can't pass in a principal to generate tokens - we expect you to be logged in at that poing. Also, ValidateRequestAsync(...) now checks the HTTP verb and won't validate GETs and such. --- .../AntiforgeryBuilderExtensions.cs | 29 ++ .../AntiforgeryMiddleware.cs | 65 +++ .../IAntiforgery.cs | 23 ++ .../AntiforgeryAuthenticationHandler.cs | 161 ++++++++ .../Internal/DefaultAntiforgery.cs | 82 +++- .../DefaultAntiforgeryTokenGenerator.cs | 10 +- .../Internal/IAntiforgeryTokenGenerator.cs | 9 +- .../Internal/TaskCache.cs | 12 + .../project.json | 3 +- .../AntiforgeryMiddlewareTest.cs | 382 ++++++++++++++++++ .../AntiforgeryAuthenticationHandlerTest.cs | 293 ++++++++++++++ .../Internal/DefaultAntiforgeryTest.cs | 262 +++++++++++- .../DefaultAntiforgeryTokenGeneratorTest.cs | 68 ++-- 13 files changed, 1339 insertions(+), 60 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Antiforgery/AntiforgeryBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Antiforgery/AntiforgeryMiddleware.cs create mode 100644 src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryAuthenticationHandler.cs create mode 100644 src/Microsoft.AspNetCore.Antiforgery/Internal/TaskCache.cs create mode 100644 test/Microsoft.AspNetCore.Antiforgery.Test/AntiforgeryMiddlewareTest.cs create mode 100644 test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryAuthenticationHandlerTest.cs diff --git a/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryBuilderExtensions.cs b/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryBuilderExtensions.cs new file mode 100644 index 0000000000..7181518dfb --- /dev/null +++ b/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryBuilderExtensions.cs @@ -0,0 +1,29 @@ +// 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.AspNetCore.Antiforgery; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods for configuring the antiforgery middleware. + /// + public static class AntiforgeryBuilderExtensions + { + /// + /// Adds the antiforgery middleware. + /// + /// The . + /// The . + public static IApplicationBuilder UseAntiforgery(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryMiddleware.cs b/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryMiddleware.cs new file mode 100644 index 0000000000..eccadfd699 --- /dev/null +++ b/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryMiddleware.cs @@ -0,0 +1,65 @@ +// 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.AspNetCore.Antiforgery.Internal; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Antiforgery +{ + /// + /// A middleware implementation of antiforgery validation. + /// + public class AntiforgeryMiddleware + { + private readonly IAntiforgery _antiforgery; + private readonly RequestDelegate _next; + + /// + /// Creates a new . + /// + /// The for the next middleware. + /// The . + public AntiforgeryMiddleware(RequestDelegate next, IAntiforgery antiforgery) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (antiforgery == null) + { + throw new ArgumentNullException(nameof(antiforgery)); + } + + _next = next; + _antiforgery = antiforgery; + } + + /// + /// Invokes the middleware for the given . + /// + /// The associated with the current request. + /// A which will be completed when execution of the middleware completes. + public async Task Invoke(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var handler = new AntiforgeryAuthenticationHandler(_antiforgery); + await handler.InitializeAsync(httpContext); + + try + { + await _next(httpContext); + } + finally + { + handler.Teardown(); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Antiforgery/IAntiforgery.cs b/src/Microsoft.AspNetCore.Antiforgery/IAntiforgery.cs index 6ac2e44e37..d22828ef31 100644 --- a/src/Microsoft.AspNetCore.Antiforgery/IAntiforgery.cs +++ b/src/Microsoft.AspNetCore.Antiforgery/IAntiforgery.cs @@ -1,6 +1,7 @@ // 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.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -46,6 +47,18 @@ namespace Microsoft.AspNetCore.Antiforgery /// Task IsRequestValidAsync(HttpContext httpContext); + /// + /// Asynchronously returns a value indicating whether the request passes antiforgery validation. If the + /// request uses a safe HTTP method (GET, HEAD, OPTIONS, TRACE), the antiforgery token is not validated. + /// + /// The associated with the current request. + /// The claims-based principal to validate. + /// + /// A that, when completed, returns true if the is requst uses a safe HTTP + /// method or contains a value antiforgery token, otherwise returns false. + /// + Task IsRequestValidAsync(HttpContext httpContext, ClaimsPrincipal principal); + /// /// Validates an antiforgery token that was supplied as part of the request. /// @@ -55,6 +68,16 @@ namespace Microsoft.AspNetCore.Antiforgery /// Task ValidateRequestAsync(HttpContext httpContext); + /// + /// Validates an antiforgery token that was supplied as part of the request. + /// + /// The associated with the current request. + /// The claims-based principal to validate. + /// + /// Thrown when the request does not include a valid antiforgery token. + /// + Task ValidateRequestAsync(HttpContext httpContext, ClaimsPrincipal principal); + /// /// Generates and stores an antiforgery cookie token if one is not available or not valid. /// diff --git a/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryAuthenticationHandler.cs b/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryAuthenticationHandler.cs new file mode 100644 index 0000000000..c3abeb0ac6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryAuthenticationHandler.cs @@ -0,0 +1,161 @@ +// 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.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.Http.Features.Authentication.Internal; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class AntiforgeryAuthenticationHandler : IAuthenticationHandler + { + private readonly IAntiforgery _antiforgery; + private HttpContext _httpContext; + private IAuthenticationHandler _priorHandler; + + public AntiforgeryAuthenticationHandler(IAntiforgery antiforgery) + { + if (antiforgery == null) + { + throw new ArgumentNullException(nameof(antiforgery)); + } + + _antiforgery = antiforgery; + } + + public async Task InitializeAsync(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + _httpContext = httpContext; + + var authentication = GetAuthenticationFeature(_httpContext); + + _priorHandler = authentication.Handler; + authentication.Handler = this; + + if (authentication.User != null) + { + if (!await _antiforgery.IsRequestValidAsync(_httpContext)) + { + // Wipe out any existing principal if we can't validate this request. + authentication.User = null; + return; + } + } + } + + public void Teardown() + { + var authentication = GetAuthenticationFeature(_httpContext); + authentication.Handler = _priorHandler; + } + + /// + public async Task AuthenticateAsync(AuthenticateContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (_priorHandler != null) + { + await _priorHandler.AuthenticateAsync(context); + + var authentication = GetAuthenticationFeature(_httpContext); + if (context.Principal != null) + { + try + { + await _antiforgery.ValidateRequestAsync(_httpContext, context.Principal); + } + catch (AntiforgeryValidationException ex) + { + context.Failed(ex); + return; + } + } + } + } + + /// + public Task ChallengeAsync(ChallengeContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (_priorHandler != null) + { + return _priorHandler.ChallengeAsync(context); + } + + return TaskCache.CompletedTask; + } + + /// + public void GetDescriptions(DescribeSchemesContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (_priorHandler != null) + { + _priorHandler.GetDescriptions(context); + } + } + + /// + public Task SignInAsync(SignInContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (_priorHandler != null) + { + return _priorHandler.SignInAsync(context); + } + + return TaskCache.CompletedTask; + } + + /// + public Task SignOutAsync(SignOutContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (_priorHandler != null) + { + return _priorHandler.SignOutAsync(context); + } + + return TaskCache.CompletedTask; + } + + private static IHttpAuthenticationFeature GetAuthenticationFeature(HttpContext httpContext) + { + var authentication = httpContext.Features.Get(); + if (authentication == null) + { + authentication = new HttpAuthenticationFeature(); + httpContext.Features.Set(authentication); + } + + return authentication; + } + } +} diff --git a/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgery.cs b/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgery.cs index 137b0b4fe0..95f0208054 100644 --- a/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgery.cs +++ b/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgery.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -85,7 +86,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal } /// - public async Task IsRequestValidAsync(HttpContext httpContext) + public Task IsRequestValidAsync(HttpContext httpContext) { if (httpContext == null) { @@ -94,13 +95,26 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal CheckSSLConfig(httpContext); - var method = httpContext.Request.Method; - if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) || - string.Equals(method, "HEAD", StringComparison.OrdinalIgnoreCase) || - string.Equals(method, "OPTIONS", StringComparison.OrdinalIgnoreCase) || - string.Equals(method, "TRACE", StringComparison.OrdinalIgnoreCase)) + return IsRequestValidAsync(httpContext, httpContext.User); + } + + /// + public async Task IsRequestValidAsync(HttpContext httpContext, ClaimsPrincipal principal) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + CheckSSLConfig(httpContext); + + if (!IsValidationRequired(httpContext)) { - // Validation not needed for these request types. return true; } @@ -126,6 +140,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal string message; var result = _tokenGenerator.TryValidateTokenSet( httpContext, + principal, deserializedCookieToken, deserializedRequestToken, out message); @@ -143,7 +158,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal } /// - public async Task ValidateRequestAsync(HttpContext httpContext) + public Task ValidateRequestAsync(HttpContext httpContext) { if (httpContext == null) { @@ -152,6 +167,29 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal CheckSSLConfig(httpContext); + return ValidateRequestAsync(httpContext, httpContext.User); + } + + /// + public async Task ValidateRequestAsync(HttpContext httpContext, ClaimsPrincipal principal) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + CheckSSLConfig(httpContext); + + if (!IsValidationRequired(httpContext)) + { + return; + } + var tokens = await _tokenStore.GetRequestTokensAsync(httpContext); if (tokens.CookieToken == null) { @@ -180,12 +218,15 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal } } - ValidateTokens(httpContext, tokens); + ValidateTokens(httpContext, principal, tokens); _logger.ValidatedAntiforgeryToken(); } - private void ValidateTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet) + private void ValidateTokens( + HttpContext httpContext, + ClaimsPrincipal principal, + AntiforgeryTokenSet antiforgeryTokenSet) { Debug.Assert(!string.IsNullOrEmpty(antiforgeryTokenSet.CookieToken)); Debug.Assert(!string.IsNullOrEmpty(antiforgeryTokenSet.RequestToken)); @@ -203,6 +244,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal string message; if (!_tokenGenerator.TryValidateTokenSet( httpContext, + principal, deserializedCookieToken, deserializedRequestToken, out message)) @@ -268,6 +310,21 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal } } + private bool IsValidationRequired(HttpContext httpContext) + { + var method = httpContext.Request.Method; + if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) || + string.Equals(method, "HEAD", StringComparison.OrdinalIgnoreCase) || + string.Equals(method, "OPTIONS", StringComparison.OrdinalIgnoreCase) || + string.Equals(method, "TRACE", StringComparison.OrdinalIgnoreCase)) + { + // Validation not needed for HTTP methods that don't mutate any state. + return false; + } + + return true; + } + private AntiforgeryContext GetCookieTokens(HttpContext httpContext) { var services = httpContext.RequestServices; @@ -340,7 +397,10 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal if (antiforgeryContext.NewRequestToken == null) { var cookieToken = antiforgeryContext.NewCookieToken ?? antiforgeryContext.CookieToken; - antiforgeryContext.NewRequestToken = _tokenGenerator.GenerateRequestToken(httpContext, cookieToken); + antiforgeryContext.NewRequestToken = _tokenGenerator.GenerateRequestToken( + httpContext, + httpContext.User, + cookieToken); } return antiforgeryContext; diff --git a/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs b/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs index 872f7ed18c..14c102bb51 100644 --- a/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs +++ b/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs @@ -35,6 +35,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal /// public AntiforgeryToken GenerateRequestToken( HttpContext httpContext, + ClaimsPrincipal principal, AntiforgeryToken cookieToken) { if (httpContext == null) @@ -63,11 +64,11 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal var isIdentityAuthenticated = false; // populate Username and ClaimUid - var authenticatedIdentity = GetAuthenticatedIdentity(httpContext.User); + var authenticatedIdentity = GetAuthenticatedIdentity(principal); if (authenticatedIdentity != null) { isIdentityAuthenticated = true; - requestToken.ClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(httpContext.User)); + requestToken.ClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(principal)); if (requestToken.ClaimUid == null) { @@ -109,6 +110,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal /// public bool TryValidateTokenSet( HttpContext httpContext, + ClaimsPrincipal principal, AntiforgeryToken cookieToken, AntiforgeryToken requestToken, out string message) @@ -150,10 +152,10 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal var currentUsername = string.Empty; BinaryBlob currentClaimUid = null; - var authenticatedIdentity = GetAuthenticatedIdentity(httpContext.User); + var authenticatedIdentity = GetAuthenticatedIdentity(principal); if (authenticatedIdentity != null) { - currentClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(httpContext.User)); + currentClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(principal)); if (currentClaimUid == null) { currentUsername = authenticatedIdentity.Name ?? string.Empty; diff --git a/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenGenerator.cs b/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenGenerator.cs index c0dff86047..8c5d22cf31 100644 --- a/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenGenerator.cs +++ b/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenGenerator.cs @@ -1,6 +1,7 @@ // 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.Security.Claims; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Antiforgery.Internal @@ -20,9 +21,13 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal /// Generates a request token corresponding to . /// /// The associated with the current request. + /// The claims-based principal to use for token generation. /// A valid cookie token. /// An . - AntiforgeryToken GenerateRequestToken(HttpContext httpContext, AntiforgeryToken cookieToken); + AntiforgeryToken GenerateRequestToken( + HttpContext httpContext, + ClaimsPrincipal principal, + AntiforgeryToken cookieToken); /// /// Attempts to validate a cookie token. @@ -35,6 +40,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal /// Attempts to validate a cookie and request token set for the given . /// /// The associated with the current request. + /// The claims-based principal to use for token validation. /// A cookie token. /// A request token. /// @@ -43,6 +49,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal /// true if the tokens are valid, otherwise false. bool TryValidateTokenSet( HttpContext httpContext, + ClaimsPrincipal principal, AntiforgeryToken cookieToken, AntiforgeryToken requestToken, out string message); diff --git a/src/Microsoft.AspNetCore.Antiforgery/Internal/TaskCache.cs b/src/Microsoft.AspNetCore.Antiforgery/Internal/TaskCache.cs new file mode 100644 index 0000000000..d21d2e7a76 --- /dev/null +++ b/src/Microsoft.AspNetCore.Antiforgery/Internal/TaskCache.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public static class TaskCache + { + public static readonly Task CompletedTask = Task.FromResult(0); + } +} diff --git a/src/Microsoft.AspNetCore.Antiforgery/project.json b/src/Microsoft.AspNetCore.Antiforgery/project.json index d5bda2ecf5..4fcfc49f40 100644 --- a/src/Microsoft.AspNetCore.Antiforgery/project.json +++ b/src/Microsoft.AspNetCore.Antiforgery/project.json @@ -13,8 +13,7 @@ }, "dependencies": { "Microsoft.AspNetCore.DataProtection": "1.0.0-*", - "Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*", - "Microsoft.AspNetCore.WebUtilities": "1.0.0-*", + "Microsoft.AspNetCore.Http": "1.0.0-*", "Microsoft.Extensions.ObjectPool": "1.0.0-*" }, "frameworks": { diff --git a/test/Microsoft.AspNetCore.Antiforgery.Test/AntiforgeryMiddlewareTest.cs b/test/Microsoft.AspNetCore.Antiforgery.Test/AntiforgeryMiddlewareTest.cs new file mode 100644 index 0000000000..56c606c847 --- /dev/null +++ b/test/Microsoft.AspNetCore.Antiforgery.Test/AntiforgeryMiddlewareTest.cs @@ -0,0 +1,382 @@ +// 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.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Antiforgery.Internal; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.Http.Features.Authentication.Internal; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Antiforgery +{ + // These are really more like integration tests and just verify a bunch of different + // reasonable combinations of authN middleware. + public class AntiforgeryMiddlewareTest + { + private readonly ClaimsPrincipal LoggedInUser = new ClaimsPrincipal(new ClaimsIdentity[] + { + new ClaimsIdentity("Test"), + }); + + private readonly ClaimsPrincipal LoggedInUser2 = new ClaimsPrincipal(new ClaimsIdentity[] + { + new ClaimsIdentity("Test"), + }); + + [Fact] + public async Task AutomaticAuthentication_Anonymous() + { + // Arrange + var context = Setup((app) => + { + app.Use(next => new AutomaticAuthenticationMiddleware(next, null).Invoke); + app.UseAntiforgery(); + }); + + var httpContext = new DefaultHttpContext(); + + await context.AppFunc(httpContext); + + Assert.Null(context.Principal); + } + + [Fact] + public async Task AutomaticAuthentication_LoggedIn_WithoutToken() + { + // Arrange + var context = Setup((app) => + { + app.UseMiddleware(LoggedInUser); + app.UseAntiforgery(); + }); + + context.Antiforgery + .Setup(a => a.IsRequestValidAsync(It.IsAny())) + .ReturnsAsync(false); + + var httpContext = new DefaultHttpContext(); + + await context.AppFunc(httpContext); + + Assert.Null(context.Principal); + } + + [Fact] + public async Task AutomaticAuthentication_LoggedIn_WithValidToken() + { + // Arrange + var context = Setup((app) => + { + app.UseMiddleware(LoggedInUser); + app.UseAntiforgery(); + }); + + context.Antiforgery + .Setup(a => a.IsRequestValidAsync(It.IsAny())) + .ReturnsAsync(true); + + var httpContext = new DefaultHttpContext(); + + await context.AppFunc(httpContext); + + Assert.Same(LoggedInUser, context.Principal); + } + + // A middleware after antiforgery in the pipeline can authenticate without going through token + // validation. + [Fact] + public async Task AutomaticAuthentication_LoggedIn_WithoutToken_AuthenticatedBySubsequentMiddleware() + { + // Arrange + var context = Setup((app) => + { + app.UseMiddleware(LoggedInUser); + app.UseAntiforgery(); + app.UseMiddleware(LoggedInUser2); + }); + + context.Antiforgery + .Setup(a => a.IsRequestValidAsync(It.IsAny())) + .ReturnsAsync(false); + + var httpContext = new DefaultHttpContext(); + + await context.AppFunc(httpContext); + + Assert.Same(LoggedInUser2, context.Principal); + } + + [Fact] + public async Task PasiveAuthentication_Anonymous() + { + // Arrange + var context = Setup((app) => + { + app.Use(next => new AuthenticationHandlerMiddleware(next, null).Invoke); + app.UseAntiforgery(); + app.UseMiddleware(); + }); + + var httpContext = new DefaultHttpContext(); + + await context.AppFunc(httpContext); + + Assert.Null(context.Principal); + } + + [Fact] + public async Task PassiveAuthentication_LoggedIn_WithoutToken() + { + // Arrange + var context = Setup((app) => + { + app.UseMiddleware(LoggedInUser); + app.UseAntiforgery(); + app.UseMiddleware(); + }); + + context.Antiforgery + .Setup(a => a.ValidateRequestAsync(It.IsAny(), LoggedInUser)) + .Throws(new AntiforgeryValidationException("error")); + + var httpContext = new DefaultHttpContext(); + + await context.AppFunc(httpContext); + + Assert.Null(context.Principal); + } + + [Fact] + public async Task PassiveAuthentication_LoggedIn_WithValidToken() + { + // Arrange + var context = Setup((app) => + { + app.UseMiddleware(LoggedInUser); + app.UseAntiforgery(); + app.UseMiddleware(); + }); + + context.Antiforgery + .Setup(a => a.ValidateRequestAsync(It.IsAny(), LoggedInUser)) + .Returns(TaskCache.CompletedTask); + + var httpContext = new DefaultHttpContext(); + + await context.AppFunc(httpContext); + + Assert.Same(LoggedInUser, context.Principal); + } + + // A middleware after antiforgery in the pipeline can authenticate without going through token + // validation. + [Fact] + public async Task PassiveAuthentication_LoggedIn_WithoutToken_AuthenticatedBySubsequentMiddleware() + { + // Arrange + var context = Setup((app) => + { + app.UseMiddleware(LoggedInUser); + app.UseAntiforgery(); + app.UseMiddleware(LoggedInUser2); + app.UseMiddleware(); + }); + + var httpContext = new DefaultHttpContext(); + + await context.AppFunc(httpContext); + + Assert.Same(LoggedInUser2, context.Principal); + } + + private static IHttpAuthenticationFeature GetAuthenticationFeature(HttpContext httpContext) + { + var authentication = httpContext.Features.Get(); + if (authentication == null) + { + authentication = new HttpAuthenticationFeature(); + httpContext.Features.Set(authentication); + } + + return authentication; + } + + private static TestContext Setup(Action action) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + + var antiforgery = new Mock(MockBehavior.Strict); + services.AddSingleton(antiforgery.Object); + + var result = new TestContext(); + result.Antiforgery = antiforgery; + + var app = new ApplicationBuilder(services.BuildServiceProvider()); + action(app); + + // Capture the logged in user 'after' the middleware so we can validate it. + app.Run(c => + { + result.Principal = GetAuthenticationFeature(c).User; + return TaskCache.CompletedTask; + }); + + result.AppFunc = app.Build(); + return result; + } + + private class TestContext + { + public Mock Antiforgery { get; set; } + + public RequestDelegate AppFunc { get; set; } + + public ClaimsPrincipal Principal { get; set; } + } + + private class AutomaticAuthenticationMiddleware + { + private readonly RequestDelegate _next; + private readonly ClaimsPrincipal _principal; + + public AutomaticAuthenticationMiddleware(RequestDelegate next, ClaimsPrincipal principal) + { + _next = next; + _principal = principal; + } + + public Task Invoke(HttpContext httpContext) + { + GetAuthenticationFeature(httpContext).User = _principal; + return _next(httpContext); + } + } + + private class AuthenticationHandlerMiddleware + { + private readonly RequestDelegate _next; + private readonly ClaimsPrincipal _principal; + + public AuthenticationHandlerMiddleware(RequestDelegate next, ClaimsPrincipal principal) + { + _next = next; + _principal = principal; + } + + public async Task Invoke(HttpContext httpContext) + { + var handler = new AuthenticationHandler(_principal); + await handler.InitializeAsync(httpContext); + + try + { + await _next(httpContext); + } + finally + { + await handler.TeardownAsync(); + } + } + } + + private class AuthenticationHandler : IAuthenticationHandler + { + private readonly ClaimsPrincipal _principal; + private IAuthenticationHandler _priorHandler; + private HttpContext _httpContext; + + public AuthenticationHandler(ClaimsPrincipal principal) + { + _principal = principal; + } + + public Task InitializeAsync(HttpContext httpContext) + { + _httpContext = httpContext; + + var authenticationFeature = GetAuthenticationFeature(_httpContext); + _priorHandler = authenticationFeature.Handler; + authenticationFeature.Handler = this; + + return TaskCache.CompletedTask; + } + + public Task TeardownAsync() + { + var authenticationFeature = GetAuthenticationFeature(_httpContext); + authenticationFeature.Handler = _priorHandler; + + return TaskCache.CompletedTask; + } + + public Task AuthenticateAsync(AuthenticateContext context) + { + if (_principal == null) + { + context.NotAuthenticated(); + } + else + { + context.Authenticated(_principal, null, null); + } + + return TaskCache.CompletedTask; + } + + public Task ChallengeAsync(ChallengeContext context) + { + throw new NotImplementedException(); + } + + public void GetDescriptions(DescribeSchemesContext context) + { + throw new NotImplementedException(); + } + + public Task SignInAsync(SignInContext context) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(SignOutContext context) + { + throw new NotImplementedException(); + } + } + + private class CallAuthenticateMiddleware + { + private readonly RequestDelegate _next; + + public CallAuthenticateMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext) + { + var authenticationFeature = GetAuthenticationFeature(httpContext); + + var authenticateContext = new AuthenticateContext("Test"); + await httpContext.Authentication.AuthenticateAsync(authenticateContext); + + if (authenticateContext.Accepted) + { + authenticationFeature.User = authenticateContext.Principal; + } + + await _next(httpContext); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryAuthenticationHandlerTest.cs b/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryAuthenticationHandlerTest.cs new file mode 100644 index 0000000000..670f818bf7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryAuthenticationHandlerTest.cs @@ -0,0 +1,293 @@ +// 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.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.Http.Features.Authentication.Internal; +using Microsoft.AspNetCore.Http.Internal; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class AntiforgeryAuthenticationHandlerTest + { + [Fact] + public async Task IntializeAsync_NoOp_WhenAnonymous() + { + // Arrange + var antiforgery = new Mock(MockBehavior.Strict); + var handler = new AntiforgeryAuthenticationHandler(antiforgery.Object); + + antiforgery + .Setup(a => a.IsRequestValidAsync(It.IsAny())) + .ReturnsAsync(false) + .Verifiable(); + + var httpContext = new DefaultHttpContext(); + + // Act + await handler.InitializeAsync(httpContext); + + // Assert + antiforgery.Verify(a => a.IsRequestValidAsync(It.IsAny()), Times.Never()); + } + + [Fact] + public async Task IntializeAsync_ValidatesRequest_WhenLoggedIn() + { + // Arrange + var antiforgery = new Mock(MockBehavior.Strict); + var handler = new AntiforgeryAuthenticationHandler(antiforgery.Object); + + antiforgery + .Setup(a => a.IsRequestValidAsync(It.IsAny())) + .ReturnsAsync(true) + .Verifiable(); + + var httpContext = new DefaultHttpContext(); + + var authenticationFeature = new HttpAuthenticationFeature(); + httpContext.Features.Set(authenticationFeature); + authenticationFeature.User = new ClaimsPrincipal(); + + // Act + await handler.InitializeAsync(httpContext); + + // Assert + antiforgery.Verify(a => a.IsRequestValidAsync(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task IntializeAsync_ClearsUser_WhenInvalid() + { + // Arrange + var antiforgery = new Mock(MockBehavior.Strict); + var handler = new AntiforgeryAuthenticationHandler(antiforgery.Object); + + antiforgery + .Setup(a => a.IsRequestValidAsync(It.IsAny())) + .ReturnsAsync(false) + .Verifiable(); + + var httpContext = new DefaultHttpContext(); + + var authenticationFeature = new HttpAuthenticationFeature(); + httpContext.Features.Set(authenticationFeature); + authenticationFeature.User = new ClaimsPrincipal(); + + // Act + await handler.InitializeAsync(httpContext); + + // Assert + Assert.Null(authenticationFeature.User); + antiforgery.Verify(a => a.IsRequestValidAsync(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task IntializeAsync_AttachesAuthorizationHandler() + { + // Arrange + var antiforgery = new Mock(MockBehavior.Strict); + var handler = new AntiforgeryAuthenticationHandler(antiforgery.Object); + + antiforgery + .Setup(a => a.IsRequestValidAsync(It.IsAny())) + .ReturnsAsync(false) + .Verifiable(); + + var httpContext = new DefaultHttpContext(); + + var authenticationFeature = new HttpAuthenticationFeature(); + httpContext.Features.Set(authenticationFeature); + + // Act + await handler.InitializeAsync(httpContext); + + // Assert + Assert.Same(handler, authenticationFeature.Handler); + antiforgery.Verify(a => a.IsRequestValidAsync(It.IsAny()), Times.Never()); + } + + [Fact] + public async Task AuthenticateAsync_NoPriorHandler_NoOp() + { + // Arrange + var antiforgery = new Mock(MockBehavior.Strict); + var handler = new AntiforgeryAuthenticationHandler(antiforgery.Object); + + antiforgery + .Setup(a => a.IsRequestValidAsync(It.IsAny())) + .ReturnsAsync(false) + .Verifiable(); + + antiforgery + .Setup(a => a.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .Verifiable(); + + var httpContext = new DefaultHttpContext(); + + var authenticationFeature = new HttpAuthenticationFeature(); + httpContext.Features.Set(authenticationFeature); + + await handler.InitializeAsync(httpContext); + + var authenticateContext = new AuthenticateContext("Test"); + + // Act + await handler.AuthenticateAsync(authenticateContext); + + // Assert + Assert.False(authenticateContext.Accepted); + + antiforgery.Verify(a => a.IsRequestValidAsync(It.IsAny()), Times.Never()); + antiforgery.Verify( + a => a.ValidateRequestAsync(It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task AuthenticateAsync_PriorHandlerDoesNotAuthenticate_NoOp() + { + // Arrange + var antiforgery = new Mock(MockBehavior.Strict); + var handler = new AntiforgeryAuthenticationHandler(antiforgery.Object); + + antiforgery + .Setup(a => a.IsRequestValidAsync(It.IsAny())) + .ReturnsAsync(false) + .Verifiable(); + + antiforgery + .Setup(a => a.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .Verifiable(); + + var httpContext = new DefaultHttpContext(); + + var authenticationFeature = new HttpAuthenticationFeature(); + httpContext.Features.Set(authenticationFeature); + var priorHandler = new Mock(MockBehavior.Strict); + authenticationFeature.Handler = priorHandler.Object; + + priorHandler + .Setup(h => h.AuthenticateAsync(It.IsAny())) + .Returns(TaskCache.CompletedTask) + .Callback(c => c.NotAuthenticated()); + + await handler.InitializeAsync(httpContext); + + var authenticateContext = new AuthenticateContext("Test"); + + // Act + await handler.AuthenticateAsync(authenticateContext); + + // Assert + Assert.True(authenticateContext.Accepted); + Assert.Null(authenticateContext.Principal); + + antiforgery.Verify(a => a.IsRequestValidAsync(It.IsAny()), Times.Never()); + antiforgery.Verify( + a => a.ValidateRequestAsync(It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task AuthenticateAsync_PriorHandlerSetsPrincipal_Valid() + { + // Arrange + var antiforgery = new Mock(MockBehavior.Strict); + var handler = new AntiforgeryAuthenticationHandler(antiforgery.Object); + + var principal = new ClaimsPrincipal(); + + antiforgery + .Setup(a => a.IsRequestValidAsync(It.IsAny())) + .ReturnsAsync(false) + .Verifiable(); + + antiforgery + .Setup(a => a.ValidateRequestAsync(It.IsAny(), principal)) + .Returns(TaskCache.CompletedTask) + .Verifiable(); + + var httpContext = new DefaultHttpContext(); + + var authenticationFeature = new HttpAuthenticationFeature(); + httpContext.Features.Set(authenticationFeature); + var priorHandler = new Mock(MockBehavior.Strict); + authenticationFeature.Handler = priorHandler.Object; + + priorHandler + .Setup(h => h.AuthenticateAsync(It.IsAny())) + .Returns(TaskCache.CompletedTask) + .Callback(c => c.Authenticated(principal, null, null)); + + await handler.InitializeAsync(httpContext); + + var authenticateContext = new AuthenticateContext("Test"); + + // Act + await handler.AuthenticateAsync(authenticateContext); + + // Assert + Assert.True(authenticateContext.Accepted); + Assert.Same(principal, authenticateContext.Principal); + + antiforgery.Verify(a => a.IsRequestValidAsync(It.IsAny()), Times.Never()); + antiforgery.Verify( + a => a.ValidateRequestAsync(It.IsAny(), principal), + Times.Once()); + } + + [Fact] + public async Task AuthenticateAsync_PriorHandlerSetsPrincipal_Invalid() + { + // Arrange + var antiforgery = new Mock(MockBehavior.Strict); + var handler = new AntiforgeryAuthenticationHandler(antiforgery.Object); + + var principal = new ClaimsPrincipal(); + + antiforgery + .Setup(a => a.IsRequestValidAsync(It.IsAny())) + .ReturnsAsync(false) + .Verifiable(); + + antiforgery + .Setup(a => a.ValidateRequestAsync(It.IsAny(), principal)) + .Throws(new AntiforgeryValidationException("invalid")) + .Verifiable(); + + var httpContext = new DefaultHttpContext(); + + var authenticationFeature = new HttpAuthenticationFeature(); + httpContext.Features.Set(authenticationFeature); + var priorHandler = new Mock(MockBehavior.Strict); + authenticationFeature.Handler = priorHandler.Object; + + priorHandler + .Setup(h => h.AuthenticateAsync(It.IsAny())) + .Returns(TaskCache.CompletedTask) + .Callback(c => c.Authenticated(principal, null, null)); + + await handler.InitializeAsync(httpContext); + + var authenticateContext = new AuthenticateContext("Test"); + + // Act + await handler.AuthenticateAsync(authenticateContext); + + // Assert + Assert.True(authenticateContext.Accepted); + Assert.Null(authenticateContext.Principal); + Assert.NotNull(authenticateContext.Error); + + antiforgery.Verify(a => a.IsRequestValidAsync(It.IsAny()), Times.Never()); + antiforgery.Verify( + a => a.ValidateRequestAsync(It.IsAny(), principal), + Times.Once()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTest.cs b/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTest.cs index 670b7f607b..2d10221a41 100644 --- a/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTest.cs +++ b/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTest.cs @@ -16,6 +16,22 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal { public class DefaultAntiforgeryTest { + public static TheoryData SafeHttpMethods => new TheoryData() + { + "GeT", + "HEAD", + "options", + "TrAcE", + }; + + public static TheoryData UnsafeHttpMethods => new TheoryData() + { + "PUT", + "post", + "Delete", + "Custom", + }; + [Fact] public async Task ChecksSSL_ValidateRequestAsync_Throws() { @@ -36,6 +52,26 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal exception.Message); } + [Fact] + public async Task ChecksSSL_ValidateRequestAsync_WithPrincipal_Throws() + { + // Arrange + var httpContext = GetHttpContext(); + var options = new AntiforgeryOptions() + { + RequireSsl = true + }; + var antiforgery = GetAntiforgery(httpContext, options); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => antiforgery.ValidateRequestAsync(httpContext, new ClaimsPrincipal())); + Assert.Equal( + @"The antiforgery system has the configuration value AntiforgeryOptions.RequireSsl = true, " + + "but the current request is not an SSL request.", + exception.Message); + } + [Fact] public async Task ChecksSSL_IsRequestValidAsync_Throws() { @@ -57,6 +93,27 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal exception.Message); } + [Fact] + public async Task ChecksSSL_IsRequestValidAsync_WithPrincipal_Throws() + { + // Arrange + var httpContext = GetHttpContext(); + var options = new AntiforgeryOptions() + { + RequireSsl = true + }; + + var antiforgery = GetAntiforgery(httpContext, options); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => antiforgery.IsRequestValidAsync(httpContext, new ClaimsPrincipal())); + Assert.Equal( + @"The antiforgery system has the configuration value AntiforgeryOptions.RequireSsl = true, " + + "but the current request is not an SSL request.", + exception.Message); + } + [Fact] public void ChecksSSL_GetAndStoreTokens_Throws() { @@ -420,6 +477,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal context.TokenGenerator .Setup(o => o.TryValidateTokenSet( context.HttpContext, + It.IsAny(), context.TestTokenSet.OldCookieToken, context.TestTokenSet.RequestToken, out message)) @@ -454,6 +512,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal context.TokenGenerator .Setup(o => o.TryValidateTokenSet( context.HttpContext, + It.IsAny(), context.TestTokenSet.OldCookieToken, context.TestTokenSet.RequestToken, out message)) @@ -497,6 +556,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal context.TokenGenerator .Setup(o => o.TryValidateTokenSet( context.HttpContext, + It.IsAny(), contextAccessor.Value.CookieToken, contextAccessor.Value.RequestToken, out message)) @@ -522,10 +582,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal } [Theory] - [InlineData("GeT")] - [InlineData("HEAD")] - [InlineData("options")] - [InlineData("TrAcE")] + [MemberData(nameof(SafeHttpMethods))] public async Task IsRequestValidAsync_SkipsAntiforgery_ForSafeHttpMethods(string httpMethod) { // Arrange @@ -536,6 +593,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal context.TokenGenerator .Setup(o => o.TryValidateTokenSet( context.HttpContext, + It.IsAny(), It.IsAny(), It.IsAny(), out message)) @@ -552,6 +610,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal context.TokenGenerator .Verify(o => o.TryValidateTokenSet( context.HttpContext, + It.IsAny(), It.IsAny(), It.IsAny(), out message), @@ -559,10 +618,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal } [Theory] - [InlineData("PUT")] - [InlineData("post")] - [InlineData("Delete")] - [InlineData("Custom")] + [MemberData(nameof(UnsafeHttpMethods))] public async Task IsRequestValidAsync_ValidatesAntiforgery_ForNonSafeHttpMethods(string httpMethod) { // Arrange @@ -573,6 +629,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal context.TokenGenerator .Setup(o => o.TryValidateTokenSet( context.HttpContext, + It.IsAny(), It.IsAny(), It.IsAny(), out message)) @@ -589,6 +646,68 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal context.TokenGenerator.Verify(); } + [Fact] + public async Task IsRequestValidAsync_UsesPrincipalFromHttpContext() + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions()); + context.HttpContext.Request.Method = "POST"; + + var principal = new ClaimsPrincipal(); + context.HttpContext.User = principal; + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + principal, + It.IsAny(), + It.IsAny(), + out message)) + .Returns(true) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + var result = await antiforgery.IsRequestValidAsync(context.HttpContext); + + // Assert + Assert.True(result); + context.TokenGenerator.Verify(); + } + + [Fact] + public async Task IsRequestValidAsync_UsesPassedInPrincipal() + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions()); + context.HttpContext.Request.Method = "POST"; + + var principal = new ClaimsPrincipal(); + context.HttpContext.User = new ClaimsPrincipal(); // This should be ignored. + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + principal, + It.IsAny(), + It.IsAny(), + out message)) + .Returns(true) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + var result = await antiforgery.IsRequestValidAsync(context.HttpContext, principal); + + // Assert + Assert.True(result); + context.TokenGenerator.Verify(); + } + [Fact] public async Task ValidateRequestAsync_FromStore_Failure() { @@ -600,6 +719,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal context.TokenGenerator .Setup(o => o.TryValidateTokenSet( context.HttpContext, + It.IsAny(), context.TestTokenSet.OldCookieToken, context.TestTokenSet.RequestToken, out message)) @@ -632,6 +752,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal context.TokenGenerator .Setup(o => o.TryValidateTokenSet( context.HttpContext, + It.IsAny(), context.TestTokenSet.OldCookieToken, context.TestTokenSet.RequestToken, out message)) @@ -776,6 +897,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal context.TokenGenerator .Setup(o => o.TryValidateTokenSet( context.HttpContext, + It.IsAny(), contextAccessor.Value.CookieToken, contextAccessor.Value.RequestToken, out message)) @@ -799,6 +921,129 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal Times.Never); } + [Theory] + [MemberData(nameof(SafeHttpMethods))] + public async Task ValidateRequestAsync_SkipsAntiforgery_ForSafeHttpMethods(string httpMethod) + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions()); + context.HttpContext.Request.Method = httpMethod; + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + It.IsAny(), + It.IsAny(), + It.IsAny(), + out message)) + .Returns(false) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + await antiforgery.ValidateRequestAsync(context.HttpContext); + + // Assert + context.TokenGenerator + .Verify(o => o.TryValidateTokenSet( + context.HttpContext, + It.IsAny(), + It.IsAny(), + It.IsAny(), + out message), + Times.Never); + } + + [Theory] + [MemberData(nameof(UnsafeHttpMethods))] + public async Task ValidateRequestAsync_ValidatesAntiforgery_ForNonSafeHttpMethods(string httpMethod) + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions()); + context.HttpContext.Request.Method = httpMethod; + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + It.IsAny(), + It.IsAny(), + It.IsAny(), + out message)) + .Returns(true) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + await antiforgery.ValidateRequestAsync(context.HttpContext); + + // Assert + context.TokenGenerator.Verify(); + } + + [Fact] + public async Task ValidateRequestAsync_UsesPrincipalFromHttpContext() + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions()); + context.HttpContext.Request.Method = "POST"; + + var principal = new ClaimsPrincipal(); + context.HttpContext.User = principal; + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + principal, + It.IsAny(), + It.IsAny(), + out message)) + .Returns(true) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + await antiforgery.ValidateRequestAsync(context.HttpContext); + + // Assert + context.TokenGenerator.Verify(); + } + + [Fact] + public async Task ValidateRequestAsync_UsesPassedInPrincipal() + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions()); + context.HttpContext.Request.Method = "POST"; + + var principal = new ClaimsPrincipal(); + context.HttpContext.User = new ClaimsPrincipal(); // This should be ignored. + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + principal, + It.IsAny(), + It.IsAny(), + out message)) + .Returns(true) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + await antiforgery.ValidateRequestAsync(context.HttpContext, principal); + + // Assert + context.TokenGenerator.Verify(); + } + [Theory] [InlineData(false, "SAMEORIGIN")] [InlineData(true, null)] @@ -1045,6 +1290,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal mockGenerator .Setup(o => o.GenerateRequestToken( httpContext, + It.IsAny(), useOldCookie ? testTokenSet.OldCookieToken : testTokenSet.NewCookieToken)) .Returns(testTokenSet.RequestToken); diff --git a/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenGeneratorTest.cs b/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenGeneratorTest.cs index 5509726a38..1720d93b95 100644 --- a/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenGeneratorTest.cs @@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Arrange var cookieToken = new AntiforgeryToken() { IsCookieToken = false }; var httpContext = new DefaultHttpContext(); - httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + var principal = new ClaimsPrincipal(new ClaimsIdentity()); Assert.False(httpContext.User.Identity.IsAuthenticated); var tokenProvider = new DefaultAntiforgeryTokenGenerator( @@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act & Assert ExceptionAssert.ThrowsArgument( - () => tokenProvider.GenerateRequestToken(httpContext, cookieToken), + () => tokenProvider.GenerateRequestToken(httpContext, principal, cookieToken), "cookieToken", "The antiforgery cookie token is invalid."); } @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Arrange var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; var httpContext = new DefaultHttpContext(); - httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + var principal = new ClaimsPrincipal(new ClaimsIdentity()); Assert.False(httpContext.User.Identity.IsAuthenticated); var tokenProvider = new DefaultAntiforgeryTokenGenerator( @@ -63,7 +63,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal additionalDataProvider: null); // Act - var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken); + var fieldToken = tokenProvider.GenerateRequestToken(httpContext, principal, cookieToken); // Assert Assert.NotNull(fieldToken); @@ -84,7 +84,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal }; var httpContext = new DefaultHttpContext(); - httpContext.User = new ClaimsPrincipal(new MyAuthenticatedIdentityWithoutUsername()); + var principal = new ClaimsPrincipal(new MyAuthenticatedIdentityWithoutUsername()); var options = new AntiforgeryOptions(); var claimUidExtractor = new Mock().Object; @@ -95,7 +95,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act & assert var exception = Assert.Throws( - () => tokenProvider.GenerateRequestToken(httpContext, cookieToken)); + () => tokenProvider.GenerateRequestToken(httpContext, principal, cookieToken)); Assert.Equal( "The provided identity of type " + $"'{typeof(MyAuthenticatedIdentityWithoutUsername).FullName}' " + @@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; var httpContext = new DefaultHttpContext(); - httpContext.User = new ClaimsPrincipal(new MyAuthenticatedIdentityWithoutUsername()); + var principal = new ClaimsPrincipal(new MyAuthenticatedIdentityWithoutUsername()); var mockAdditionalDataProvider = new Mock(); mockAdditionalDataProvider.Setup(o => o.GetAdditionalData(httpContext)) @@ -128,7 +128,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal additionalDataProvider: mockAdditionalDataProvider.Object); // Act - var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken); + var fieldToken = tokenProvider.GenerateRequestToken(httpContext, principal, cookieToken); // Assert Assert.NotNull(fieldToken); @@ -147,7 +147,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal var identity = GetAuthenticatedIdentity("some-identity"); var httpContext = new DefaultHttpContext(); - httpContext.User = new ClaimsPrincipal(identity); + var principal = new ClaimsPrincipal(identity); byte[] data = new byte[256 / 8]; using (var rng = RandomNumberGenerator.Create()) @@ -166,7 +166,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal additionalDataProvider: null); // Act - var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken); + var fieldToken = tokenProvider.GenerateRequestToken(httpContext, principal, cookieToken); // Assert Assert.NotNull(fieldToken); @@ -190,7 +190,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal mockIdentity.Setup(o => o.Name) .Returns("my-username"); - httpContext.User = new ClaimsPrincipal(mockIdentity.Object); + var principal = new ClaimsPrincipal(mockIdentity.Object); var claimUidExtractor = new Mock().Object; @@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal additionalDataProvider: null); // Act - var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken); + var fieldToken = tokenProvider.GenerateRequestToken(httpContext, principal, cookieToken); // Assert Assert.NotNull(fieldToken); @@ -272,7 +272,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal { // Arrange var httpContext = new DefaultHttpContext(); - httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + var principal = new ClaimsPrincipal(new ClaimsIdentity()); var fieldtoken = new AntiforgeryToken() { IsCookieToken = false }; @@ -283,7 +283,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act & Assert string message; var ex = Assert.Throws( - () => tokenProvider.TryValidateTokenSet(httpContext, null, fieldtoken, out message)); + () => tokenProvider.TryValidateTokenSet(httpContext, principal, null, fieldtoken, out message)); var trimmed = ex.Message.Substring(0, ex.Message.IndexOf(Environment.NewLine)); Assert.Equal(@"The required antiforgery cookie token must be provided.", trimmed); @@ -294,7 +294,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal { // Arrange var httpContext = new DefaultHttpContext(); - httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + var principal = new ClaimsPrincipal(new ClaimsIdentity()); var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; @@ -306,7 +306,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act & Assert string message; var ex = Assert.Throws( - () => tokenProvider.TryValidateTokenSet(httpContext, cookieToken, null, out message)); + () => tokenProvider.TryValidateTokenSet(httpContext, principal, cookieToken, null, out message)); var trimmed = ex.Message.Substring(0, ex.Message.IndexOf(Environment.NewLine)); Assert.Equal("The required antiforgery request token must be provided.", trimmed); @@ -317,7 +317,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal { // Arrange var httpContext = new DefaultHttpContext(); - httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + var principal = new ClaimsPrincipal(new ClaimsIdentity()); var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; var fieldtoken = new AntiforgeryToken() { IsCookieToken = false }; @@ -332,7 +332,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act string message; - var result = tokenProvider.TryValidateTokenSet(httpContext, fieldtoken, fieldtoken, out message); + var result = tokenProvider.TryValidateTokenSet(httpContext, principal, fieldtoken, fieldtoken, out message); // Assert Assert.False(result); @@ -344,7 +344,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal { // Arrange var httpContext = new DefaultHttpContext(); - httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + var principal = new ClaimsPrincipal(new ClaimsIdentity()); var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; var fieldtoken = new AntiforgeryToken() { IsCookieToken = false }; @@ -359,7 +359,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act string message; - var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, cookieToken, out message); + var result = tokenProvider.TryValidateTokenSet(httpContext, principal, cookieToken, cookieToken, out message); // Assert Assert.False(result); @@ -371,7 +371,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal { // Arrange var httpContext = new DefaultHttpContext(); - httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + var principal = new ClaimsPrincipal(new ClaimsIdentity()); var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; var fieldtoken = new AntiforgeryToken() { IsCookieToken = false }; @@ -384,7 +384,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act string message; - var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + var result = tokenProvider.TryValidateTokenSet(httpContext, principal, cookieToken, fieldtoken, out message); // Assert Assert.False(result); @@ -400,7 +400,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Arrange var httpContext = new DefaultHttpContext(); var identity = GetAuthenticatedIdentity(identityUsername); - httpContext.User = new ClaimsPrincipal(identity); + var principal = new ClaimsPrincipal(identity); var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; var fieldtoken = new AntiforgeryToken() @@ -424,7 +424,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act string message; - var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + var result = tokenProvider.TryValidateTokenSet(httpContext, principal, cookieToken, fieldtoken, out message); // Assert Assert.False(result); @@ -437,7 +437,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Arrange var httpContext = new DefaultHttpContext(); var identity = GetAuthenticatedIdentity("the-user"); - httpContext.User = new ClaimsPrincipal(identity); + var principal = new ClaimsPrincipal(identity); var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; var fieldtoken = new AntiforgeryToken() @@ -462,7 +462,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act string message; - var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + var result = tokenProvider.TryValidateTokenSet(httpContext, principal, cookieToken, fieldtoken, out message); // Assert Assert.False(result); @@ -475,7 +475,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Arrange var httpContext = new DefaultHttpContext(); var identity = new ClaimsIdentity(); - httpContext.User = new ClaimsPrincipal(identity); + var principal = new ClaimsPrincipal(identity); var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; var fieldtoken = new AntiforgeryToken() @@ -499,7 +499,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act string message; - var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + var result = tokenProvider.TryValidateTokenSet(httpContext, principal, cookieToken, fieldtoken, out message); // Assert Assert.False(result); @@ -512,7 +512,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Arrange var httpContext = new DefaultHttpContext(); var identity = new ClaimsIdentity(); - httpContext.User = new ClaimsPrincipal(identity); + var principal = new ClaimsPrincipal(identity); var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; var fieldtoken = new AntiforgeryToken() @@ -533,7 +533,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act string message; - var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + var result = tokenProvider.TryValidateTokenSet(httpContext, principal, cookieToken, fieldtoken, out message); // Assert Assert.True(result); @@ -546,7 +546,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Arrange var httpContext = new DefaultHttpContext(); var identity = GetAuthenticatedIdentity("the-user"); - httpContext.User = new ClaimsPrincipal(identity); + var principal = new ClaimsPrincipal(identity); var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; var fieldtoken = new AntiforgeryToken() @@ -567,7 +567,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act string message; - var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + var result = tokenProvider.TryValidateTokenSet(httpContext, principal, cookieToken, fieldtoken, out message); // Assert Assert.True(result); @@ -580,7 +580,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Arrange var httpContext = new DefaultHttpContext(); var identity = GetAuthenticatedIdentity("the-user"); - httpContext.User = new ClaimsPrincipal(identity); + var principal = new ClaimsPrincipal(identity); var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; var fieldtoken = new AntiforgeryToken() @@ -600,7 +600,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal // Act string message; - var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + var result = tokenProvider.TryValidateTokenSet(httpContext, principal, cookieToken, fieldtoken, out message); // Assert Assert.True(result);