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);