diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs index f747354e7b..ae7e866fb2 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs @@ -323,6 +323,41 @@ namespace Microsoft.AspNet.Authentication.Cookies protected override void ApplyResponseChallenge() { + if (ShouldConvertChallengeToForbidden()) + { + // Handle 403 by redirecting to AccessDeniedPath if set + if (Options.AccessDeniedPath.HasValue) + { + try + { + var accessDeniedUri = + Request.Scheme + + "://" + + Request.Host + + Request.PathBase + + Options.AccessDeniedPath; + + var redirectContext = new CookieApplyRedirectContext(Context, Options, accessDeniedUri); + Options.Notifications.ApplyRedirect(redirectContext); + } + catch (Exception exception) + { + var exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.ApplyResponseChallenge, exception, ticket: null); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) + { + throw; + } + } + } + else + { + Response.StatusCode = 403; + } + return; + } + if (Response.StatusCode != 401 || !Options.LoginPath.HasValue ) { return; diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs index 21df592538..e3ef3f5700 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs @@ -4,8 +4,6 @@ using System; using Microsoft.AspNet.Authentication.Cookies.Infrastructure; using Microsoft.AspNet.Authentication.DataHandler; -using Microsoft.AspNet.Authentication.Cookies.Infrastructure; -using Microsoft.AspNet.Authentication.DataHandler; using Microsoft.AspNet.Builder; using Microsoft.AspNet.DataProtection; using Microsoft.Framework.Logging; diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs index 12ff4f9956..02e9743b4a 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs @@ -105,6 +105,15 @@ namespace Microsoft.AspNet.Authentication.Cookies [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Logout", Justification = "By design")] public PathString LogoutPath { get; set; } + /// + /// The AccessDeniedPath property informs the middleware that it should change an outgoing 403 Forbidden status + /// code into a 302 redirection onto the given path. + /// + /// If the AccessDeniedPath is null or empty, the middleware will not look for 403 Forbidden status codes, and it will + /// not redirect + /// + public PathString AccessDeniedPath { get; set; } + /// /// The ReturnUrlParameter determines the name of the query string parameter which is appended by the middleware /// when a 401 Unauthorized status code is changed to a 302 redirect onto the login path. This is also the query diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs index 42e6f838fb..7f41be810c 100644 --- a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs @@ -7,10 +7,9 @@ using System.IdentityModel.Tokens; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.Notifications; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; -using Microsoft.AspNet.Authentication; -using Microsoft.AspNet.Authentication.Notifications; using Microsoft.Framework.Logging; using Microsoft.IdentityModel.Protocols; @@ -195,6 +194,7 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer if (ShouldConvertChallengeToForbidden()) { Response.StatusCode = 403; + return; } if ((Response.StatusCode != 401) || (ChallengeContext == null)) diff --git a/src/Microsoft.AspNet.Authentication/AutomaticAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/AutomaticAuthenticationHandler.cs index 5032e47fae..d91fadfcad 100644 --- a/src/Microsoft.AspNet.Authentication/AutomaticAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication/AutomaticAuthenticationHandler.cs @@ -90,23 +90,5 @@ namespace Microsoft.AspNet.Authentication return Options.AutomaticAuthentication && string.IsNullOrWhiteSpace(authenticationScheme); } - - /// - /// Override this method to deal with 401 challenge concerns, if an authentication scheme in question - /// deals an authentication interaction as part of it's request flow. (like adding a response header, or - /// changing the 401 result to 302 of a login page or external sign-in location.) - /// - /// - protected override Task ApplyResponseChallengeAsync() - { - // If authenticate was called and the the status is still 401, authZ failed so set 403 and stop - if (ShouldConvertChallengeToForbidden()) - { - Response.StatusCode = 403; - return Task.FromResult(0); - } - return base.ApplyResponseChallengeAsync(); - } - } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs index c1d5a0c329..51d9a9452c 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs @@ -420,6 +420,27 @@ namespace Microsoft.AspNet.Authentication.Cookies transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); } + [Fact] + public async Task CookieTurns401ToAccessDeniedWhenSetAndIfAuthenticated() + { + var clock = new TestClock(); + TestServer server = CreateServer(options => + { + options.SystemClock = clock; + options.AccessDeniedPath = new PathString("/accessdenied"); + }, + SignInAsAlice); + + Transaction transaction1 = await SendAsync(server, "http://example.com/testpath"); + + Transaction transaction2 = await SendAsync(server, "http://example.com/unauthorized", transaction1.CookieNameValue); + + transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + + Uri location = transaction2.Response.Headers.Location; + location.LocalPath.ShouldBe("/accessdenied"); + } + [Fact] public async Task CookieDoesNothingTo401IfNotAuthenticated() {