diff --git a/src/Microsoft.AspNet.Mvc.Core/ForbiddenResult.cs b/src/Microsoft.AspNet.Mvc.Core/ForbiddenResult.cs new file mode 100644 index 0000000000..b5a24eba46 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ForbiddenResult.cs @@ -0,0 +1,122 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Mvc.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// An that on execution issues a 403 forbidden response + /// if the authentication challenge is unacceptable. + /// + public class ForbiddenResult : ActionResult + { + /// + /// Initializes a new instance of . + /// + public ForbiddenResult() + : this(new string[] { }) + { + } + + /// + /// Initializes a new instance of with the + /// specified authentication scheme. + /// + /// The authentication scheme to challenge. + public ForbiddenResult(string authenticationScheme) + : this(new[] { authenticationScheme }) + { + } + + /// + /// Initializes a new instance of with the + /// specified authentication schemes. + /// + /// The authentication schemes to challenge. + public ForbiddenResult(IList authenticationSchemes) + : this(authenticationSchemes, properties: null) + { + } + + /// + /// Initializes a new instance of with the + /// specified . + /// + /// used to perform the authentication + /// challenge. + public ForbiddenResult(AuthenticationProperties properties) + : this(new string[] { }, properties) + { + } + + /// + /// Initializes a new instance of with the + /// specified authentication scheme and . + /// + /// The authentication schemes to challenge. + /// used to perform the authentication + /// challenge. + public ForbiddenResult(string authenticationScheme, AuthenticationProperties properties) + : this(new[] { authenticationScheme }, properties) + { + } + + /// + /// Initializes a new instance of with the + /// specified authentication schemes and . + /// + /// The authentication scheme to challenge. + /// used to perform the authentication + /// challenge. + public ForbiddenResult(IList authenticationSchemes, AuthenticationProperties properties) + { + AuthenticationSchemes = authenticationSchemes; + Properties = properties; + } + + /// + /// Gets or sets the authentication schemes that are challenged. + /// + public IList AuthenticationSchemes { get; set; } + + /// + /// Gets or sets the used to perform the authentication challenge. + /// + public AuthenticationProperties Properties { get; set; } + + /// + public override async Task ExecuteResultAsync(ActionContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var loggerFactory = context.HttpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + + var authentication = context.HttpContext.Authentication; + + if (AuthenticationSchemes != null && AuthenticationSchemes.Count > 0) + { + for (var i = 0; i < AuthenticationSchemes.Count; i++) + { + await authentication.ForbidAsync(AuthenticationSchemes[i], Properties); + } + } + else + { + await authentication.ForbidAsync(Properties); + } + + logger.ForbiddenResultExecuting(AuthenticationSchemes); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/ForbiddenResultLoggerExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/ForbiddenResultLoggerExtensions.cs new file mode 100644 index 0000000000..40989c59a3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Logging/ForbiddenResultLoggerExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNet.Mvc.Logging +{ + internal static class ForbiddenResultLoggerExtensions + { + private static readonly Action _resultExecuting = + LoggerMessage.Define( + LogLevel.Information, + eventId: 1, + formatString: $"Executing {nameof(ForbiddenResult)} with authentication schemes ({{Schemes}})."); + + public static void ForbiddenResultExecuting(this ILogger logger, IList authenticationSchemes) + { + _resultExecuting(logger, authenticationSchemes.ToArray(), null); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ForbiddenResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ForbiddenResultTest.cs new file mode 100644 index 0000000000..a028aac4d6 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ForbiddenResultTest.cs @@ -0,0 +1,154 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Mvc.Abstractions; +using Microsoft.AspNet.Mvc.Internal; +using Microsoft.AspNet.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class ForbiddenResultTest + { + [Fact] + public async Task ExecuteResultAsync_InvokesForbiddenAsyncOnAuthenticationManager() + { + // Arrange + var authenticationManager = new Mock(); + authenticationManager + .Setup(c => c.ForbidAsync("", null)) + .Returns(TaskCache.CompletedTask) + .Verifiable(); + var httpContext = new Mock(); + httpContext.Setup(c => c.RequestServices).Returns(CreateServices()); + httpContext.Setup(c => c.Authentication).Returns(authenticationManager.Object); + var result = new ForbiddenResult("", null); + var routeData = new RouteData(); + + var actionContext = new ActionContext( + httpContext.Object, + routeData, + new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + authenticationManager.Verify(); + } + + [Fact] + public async Task ExecuteResultAsync_InvokesForbiddenAsyncOnAllConfiguredSchemes() + { + // Arrange + var authProperties = new AuthenticationProperties(); + var authenticationManager = new Mock(); + authenticationManager + .Setup(c => c.ForbidAsync("Scheme1", authProperties)) + .Returns(TaskCache.CompletedTask) + .Verifiable(); + authenticationManager + .Setup(c => c.ForbidAsync("Scheme2", authProperties)) + .Returns(TaskCache.CompletedTask) + .Verifiable(); + var httpContext = new Mock(); + httpContext.Setup(c => c.RequestServices).Returns(CreateServices()); + httpContext.Setup(c => c.Authentication).Returns(authenticationManager.Object); + var result = new ForbiddenResult(new[] { "Scheme1", "Scheme2" }, authProperties); + var routeData = new RouteData(); + + var actionContext = new ActionContext( + httpContext.Object, + routeData, + new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + authenticationManager.Verify(); + } + + public static TheoryData ExecuteResultAsync_InvokesForbiddenAsyncWithAuthPropertiesData => + new TheoryData + { + null, + new AuthenticationProperties() + }; + + [Theory] + [MemberData(nameof(ExecuteResultAsync_InvokesForbiddenAsyncWithAuthPropertiesData))] + public async Task ExecuteResultAsync_InvokesForbiddenAsyncWithAuthProperties(AuthenticationProperties expected) + { + // Arrange + var authenticationManager = new Mock(); + authenticationManager + .Setup(c => c.ForbidAsync(expected)) + .Returns(TaskCache.CompletedTask) + .Verifiable(); + var httpContext = new Mock(); + httpContext.Setup(c => c.RequestServices).Returns(CreateServices()); + httpContext.Setup(c => c.Authentication).Returns(authenticationManager.Object); + var result = new ForbiddenResult(expected); + var routeData = new RouteData(); + + var actionContext = new ActionContext( + httpContext.Object, + routeData, + new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + authenticationManager.Verify(); + } + + [Theory] + [MemberData(nameof(ExecuteResultAsync_InvokesForbiddenAsyncWithAuthPropertiesData))] + public async Task ExecuteResultAsync_InvokesForbiddenAsyncWithAuthProperties_WhenAuthenticationSchemesIsEmpty( + AuthenticationProperties expected) + { + // Arrange + var authenticationManager = new Mock(); + authenticationManager + .Setup(c => c.ForbidAsync(expected)) + .Returns(TaskCache.CompletedTask) + .Verifiable(); + var httpContext = new Mock(); + httpContext.Setup(c => c.RequestServices).Returns(CreateServices()); + httpContext.Setup(c => c.Authentication).Returns(authenticationManager.Object); + var result = new ForbiddenResult(expected) + { + AuthenticationSchemes = new string[0] + }; + var routeData = new RouteData(); + + var actionContext = new ActionContext( + httpContext.Object, + routeData, + new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + authenticationManager.Verify(); + } + + private static IServiceProvider CreateServices() + { + return new ServiceCollection() + .AddInstance(NullLoggerFactory.Instance) + .BuildServiceProvider(); + } + } +} \ No newline at end of file