Use ASP NET Core's AuthenticationScheme to handle AbpAuthorizationException.

Resolve #9926
pull/9940/head
maliming 4 years ago
parent 22042a2b8c
commit 74001e550e

@ -5,10 +5,12 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.ExceptionHandling;
using Volo.Abp.Authorization;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Http;
@ -55,17 +57,10 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
//TODO: Trigger an AbpExceptionHandled event or something like that.
context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
context.HttpContext.Response.StatusCode = (int) context
.GetRequiredService<IHttpExceptionStatusCodeFinder>()
.GetStatusCode(context.HttpContext, context.Exception);
var exceptionHandlingOptions = context.GetRequiredService<IOptions<AbpExceptionHandlingOptions>>().Value;
var exceptionToErrorInfoConverter = context.GetRequiredService<IExceptionToErrorInfoConverter>();
var remoteServiceErrorInfo = exceptionToErrorInfoConverter.Convert(context.Exception, exceptionHandlingOptions.SendExceptionsDetailsToClients);
context.Result = new ObjectResult(new RemoteServiceErrorResponse(remoteServiceErrorInfo));
var logLevel = context.Exception.GetLogLevel();
var remoteServiceErrorInfoBuilder = new StringBuilder();
@ -80,6 +75,23 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
await context.GetRequiredService<IExceptionNotifier>().NotifyAsync(new ExceptionNotificationContext(context.Exception));
if (context.Exception is AbpAuthorizationException)
{
if (await context.HttpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>()
.HandleAsync(context.Exception.As<AbpAuthorizationException>(), context.HttpContext))
{
context.Exception = null; //Handled!
return;
}
}
context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
context.HttpContext.Response.StatusCode = (int) context
.GetRequiredService<IHttpExceptionStatusCodeFinder>()
.GetStatusCode(context.HttpContext, context.Exception);
context.Result = new ObjectResult(new RemoteServiceErrorResponse(remoteServiceErrorInfo));
context.Exception = null; //Handled!
}
}

@ -5,10 +5,12 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.ExceptionHandling;
using Volo.Abp.Authorization;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Http;
@ -67,17 +69,10 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
//TODO: Trigger an AbpExceptionHandled event or something like that.
context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
context.HttpContext.Response.StatusCode = (int) context
.GetRequiredService<IHttpExceptionStatusCodeFinder>()
.GetStatusCode(context.HttpContext, context.Exception);
var exceptionHandlingOptions = context.GetRequiredService<IOptions<AbpExceptionHandlingOptions>>().Value;
var exceptionToErrorInfoConverter = context.GetRequiredService<IExceptionToErrorInfoConverter>();
var remoteServiceErrorInfo = exceptionToErrorInfoConverter.Convert(context.Exception, exceptionHandlingOptions.SendExceptionsDetailsToClients);
context.Result = new ObjectResult(new RemoteServiceErrorResponse(remoteServiceErrorInfo));
var logLevel = context.Exception.GetLogLevel();
var remoteServiceErrorInfoBuilder = new StringBuilder();
@ -91,6 +86,23 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
await context.GetRequiredService<IExceptionNotifier>().NotifyAsync(new ExceptionNotificationContext(context.Exception));
if (context.Exception is AbpAuthorizationException)
{
if (await context.HttpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>()
.HandleAsync(context.Exception.As<AbpAuthorizationException>(), context.HttpContext))
{
context.Exception = null; //Handled!
return;
}
}
context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
context.HttpContext.Response.StatusCode = (int) context
.GetRequiredService<IHttpExceptionStatusCodeFinder>()
.GetStatusCode(context.HttpContext, context.Exception);
context.Result = new ObjectResult(new RemoteServiceErrorResponse(remoteServiceErrorInfo));
context.Exception = null; //Handled!
}
}

@ -0,0 +1,17 @@
namespace Volo.Abp.AspNetCore.ExceptionHandling
{
public class AbpAuthorizationExceptionHandlerOptions
{
public bool UseAuthenticationScheme { get; set; }
/// <summary>
/// Use default forbid/challenge scheme if this is not specified.
/// </summary>
public string AuthenticationScheme { get; set; }
public AbpAuthorizationExceptionHandlerOptions()
{
UseAuthenticationScheme = true;
}
}
}

@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Authorization;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Http;
@ -58,6 +59,22 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling
{
_logger.LogException(exception);
await httpContext
.RequestServices
.GetRequiredService<IExceptionNotifier>()
.NotifyAsync(
new ExceptionNotificationContext(exception)
);
if (exception is AbpAuthorizationException)
{
if (await httpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>()
.HandleAsync(exception.As<AbpAuthorizationException>(), httpContext))
{
return;
}
}
var errorInfoConverter = httpContext.RequestServices.GetRequiredService<IExceptionToErrorInfoConverter>();
var statusCodeFinder = httpContext.RequestServices.GetRequiredService<IHttpExceptionStatusCodeFinder>();
var jsonSerializer = httpContext.RequestServices.GetRequiredService<IJsonSerializer>();
@ -75,13 +92,6 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling
)
)
);
await httpContext
.RequestServices
.GetRequiredService<IExceptionNotifier>()
.NotifyAsync(
new ExceptionNotificationContext(exception)
);
}
private Task ClearCacheHeaders(object state)

@ -0,0 +1,68 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.Authorization;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.ExceptionHandling
{
public class DefaultAbpAuthorizationExceptionHandler : IAbpAuthorizationExceptionHandler, ITransientDependency
{
public virtual async Task<bool> HandleAsync(AbpAuthorizationException exception, HttpContext httpContext)
{
var isAuthenticated = httpContext.User.Identity?.IsAuthenticated ?? false;
var handlerOptions = httpContext.RequestServices.GetRequiredService<IOptions<AbpAuthorizationExceptionHandlerOptions>>().Value;
if (handlerOptions.UseAuthenticationScheme)
{
var handlers = httpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
if (!handlerOptions.AuthenticationScheme.IsNullOrWhiteSpace())
{
var handler = await handlers.GetHandlerAsync(httpContext, handlerOptions.AuthenticationScheme);
if (handler != null)
{
if (isAuthenticated)
{
await handler.ForbidAsync(null);
}
else
{
await handler.ChallengeAsync(null);
}
return true;
}
}
var authenticationSchemeProvider = httpContext.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>();
var scheme = isAuthenticated
? await authenticationSchemeProvider.GetDefaultForbidSchemeAsync()
: await authenticationSchemeProvider.GetDefaultChallengeSchemeAsync();
if (scheme != null)
{
var handler = await handlers.GetHandlerAsync(httpContext, scheme.Name);
if (handler != null)
{
if (isAuthenticated)
{
await handler.ForbidAsync(null);
}
else
{
await handler.ChallengeAsync(null);
}
return true;
}
}
}
return false;
}
}
}

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Volo.Abp.Authorization;
namespace Volo.Abp.AspNetCore.ExceptionHandling
{
public interface IAbpAuthorizationExceptionHandler
{
Task<bool> HandleAsync(AbpAuthorizationException exception, HttpContext httpContext);
}
}

@ -21,6 +21,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(MicrosoftPackageVersion)" />
</ItemGroup>
<ItemGroup>

@ -53,6 +53,12 @@ namespace Volo.Abp.AspNetCore.Mvc
.EnableAll();
});
context.Services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "Bearer";
options.DefaultForbidScheme = "Cookie";
}).AddCookie("Cookie").AddJwtBearer("Bearer", _ => { });
context.Services.AddAuthorization(options =>
{
options.AddPolicy("MyClaimTestPolicy", policy =>
@ -116,6 +122,7 @@ namespace Volo.Abp.AspNetCore.Mvc
app.UseRouting();
app.UseMiddleware<FakeAuthenticationMiddleware>();
app.UseAbpClaimsMap();
app.UseAuthentication();
app.UseAuthorization();
app.UseAuditing();
app.UseUnitOfWork();

@ -1,4 +1,5 @@
using System;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using Shouldly;
@ -57,10 +58,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Authorization
new Claim("MyCustomClaimType", "43")
});
//TODO: We can get a real exception if we properly configure authentication schemas for this project
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await GetResponseAsStringAsync("/AuthTest/CustomPolicyTest")
);
await GetResponseAsStringAsync("/AuthTest/CustomPolicyTest", HttpStatusCode.Redirect);
}
[Fact]

@ -0,0 +1,70 @@
using System;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NSubstitute;
using Shouldly;
using Volo.Abp.AspNetCore.ExceptionHandling;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Security.Claims;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
public class AbpAuthorizationExceptionTestController_Tests : AspNetCoreMvcTestBase
{
protected IExceptionSubscriber FakeExceptionSubscriber;
protected FakeUserClaims FakeRequiredService;
public AbpAuthorizationExceptionTestController_Tests()
{
FakeRequiredService = GetRequiredService<FakeUserClaims>();
}
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
base.ConfigureServices(context, services);
FakeExceptionSubscriber = Substitute.For<IExceptionSubscriber>();
services.AddSingleton(FakeExceptionSubscriber);
services.Configure<AbpAuthorizationExceptionHandlerOptions>(options =>
{
options.UseAuthenticationScheme = true;
options.AuthenticationScheme = "Cookie";
});
}
[Fact]
public virtual async Task Should_Handle_By_Cookie_AuthenticationScheme_For_AbpAuthorizationException()
{
var result = await GetResponseAsync("/api/exception-test/AbpAuthorizationException", HttpStatusCode.Redirect);
result.Headers.Location.ToString().ShouldContain("http://localhost/Account/Login");
#pragma warning disable 4014
FakeExceptionSubscriber
.Received()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
FakeRequiredService.Claims.AddRange(new[]
{
new Claim(AbpClaimTypes.UserId, Guid.NewGuid().ToString())
});
result = await GetResponseAsync("/api/exception-test/AbpAuthorizationException", HttpStatusCode.Redirect);
result.Headers.Location.ToString().ShouldContain("http://localhost/Account/AccessDenied");
#pragma warning disable 4014
FakeExceptionSubscriber
.Received()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
}
}
}

@ -0,0 +1,70 @@
using System;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NSubstitute;
using Shouldly;
using Volo.Abp.AspNetCore.ExceptionHandling;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Security.Claims;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
public class AbpAuthorizationExceptionTestPage_Tests : AspNetCoreMvcTestBase
{
private IExceptionSubscriber _fakeExceptionSubscriber;
private FakeUserClaims _fakeRequiredService;
public AbpAuthorizationExceptionTestPage_Tests()
{
_fakeRequiredService = GetRequiredService<FakeUserClaims>();
}
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
base.ConfigureServices(context, services);
_fakeExceptionSubscriber = Substitute.For<IExceptionSubscriber>();
services.AddSingleton(_fakeExceptionSubscriber);
services.Configure<AbpAuthorizationExceptionHandlerOptions>(options =>
{
options.UseAuthenticationScheme = true;
options.AuthenticationScheme = "Cookie";
});
}
[Fact]
public virtual async Task Should_Handle_By_Cookie_AuthenticationScheme_For_AbpAuthorizationException()
{
var result = await GetResponseAsync("/ExceptionHandling/ExceptionTestPage?handler=AbpAuthorizationException", HttpStatusCode.Redirect);
result.Headers.Location.ToString().ShouldContain("http://localhost/Account/Login");
#pragma warning disable 4014
_fakeExceptionSubscriber
.Received()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
_fakeRequiredService.Claims.AddRange(new[]
{
new Claim(AbpClaimTypes.UserId, Guid.NewGuid().ToString())
});
result = await GetResponseAsync("/ExceptionHandling/ExceptionTestPage?handler=AbpAuthorizationException", HttpStatusCode.Redirect);
result.Headers.Location.ToString().ShouldContain("http://localhost/Account/AccessDenied");
#pragma warning disable 4014
_fakeExceptionSubscriber
.Received()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
}
}
}

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Authorization;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
@ -18,5 +19,12 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
throw new UserFriendlyException("This is a sample exception!");
}
[HttpGet]
[Route("AbpAuthorizationException")]
public void AbpAuthorizationException()
{
throw new AbpAuthorizationException("This is a sample exception!");
}
}
}

@ -1,4 +1,6 @@
using System.Net;
using System;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -6,6 +8,7 @@ using NSubstitute;
using Shouldly;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Http;
using Volo.Abp.Security.Claims;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
@ -14,6 +17,13 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
private IExceptionSubscriber _fakeExceptionSubscriber;
private FakeUserClaims FakeRequiredService;
public ExceptionTestController_Tests()
{
FakeRequiredService = GetRequiredService<FakeUserClaims>();
}
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
base.ConfigureServices(context, services);
@ -50,6 +60,37 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
_fakeExceptionSubscriber
.DidNotReceive()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
}
[Fact]
public virtual async Task Should_Handle_By_Cookie_AuthenticationScheme_For_AbpAuthorizationException_For_Void_Return_Value()
{
FakeRequiredService.Claims.AddRange(new[]
{
new Claim(AbpClaimTypes.UserId, Guid.NewGuid().ToString())
});
var result = await GetResponseAsync("/api/exception-test/AbpAuthorizationException", HttpStatusCode.Redirect);
result.Headers.Location.ToString().ShouldContain("http://localhost/Account/AccessDenied");
#pragma warning disable 4014
_fakeExceptionSubscriber
.Received()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
}
[Fact]
public virtual async Task Should_Handle_By_JwtBearer_AuthenticationScheme_For_AbpAuthorizationException_For_Void_Return_Value()
{
var result = await GetResponseAsync("/api/exception-test/AbpAuthorizationException", HttpStatusCode.Unauthorized);
result.Headers.WwwAuthenticate.ToString().ShouldBe("Bearer");
#pragma warning disable 4014
_fakeExceptionSubscriber
.Received()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
}
}

@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.Authorization;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
@ -31,5 +32,9 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
throw new UserFriendlyException("This is a sample exception!");
}
public Task<JsonResult> OnGetAbpAuthorizationException()
{
throw new AbpAuthorizationException("This is a sample exception!");
}
}
}

@ -1,4 +1,6 @@
using System;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -6,6 +8,7 @@ using NSubstitute;
using Shouldly;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Http;
using Volo.Abp.Security.Claims;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
@ -14,6 +17,13 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
private IExceptionSubscriber _fakeExceptionSubscriber;
private FakeUserClaims _fakeRequiredService;
public ExceptionTestPage_Tests()
{
_fakeRequiredService = GetRequiredService<FakeUserClaims>();
}
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
base.ConfigureServices(context, services);
@ -92,6 +102,38 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
result.Error.ShouldNotBeNull();
result.Error.Message.ShouldBe("This is a sample exception!");
#pragma warning disable 4014
_fakeExceptionSubscriber
.Received()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
}
[Fact]
public virtual async Task Should_Handle_By_Cookie_AuthenticationScheme_For_AbpAuthorizationException_For_Void_Return_Value()
{
_fakeRequiredService.Claims.AddRange(new[]
{
new Claim(AbpClaimTypes.UserId, Guid.NewGuid().ToString())
});
var result = await GetResponseAsync("/ExceptionHandling/ExceptionTestPage?handler=AbpAuthorizationException", HttpStatusCode.Redirect);
result.Headers.Location.ToString().ShouldContain("http://localhost/Account/AccessDenied");
#pragma warning disable 4014
_fakeExceptionSubscriber
.Received()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
#pragma warning restore 4014
}
[Fact]
public virtual async Task Should_Handle_By_JwtBearer_AuthenticationScheme_For_AbpAuthorizationException_For_Void_Return_Value()
{
var result = await GetResponseAsync("/ExceptionHandling/ExceptionTestPage?handler=AbpAuthorizationException", HttpStatusCode.Unauthorized);
result.Headers.WwwAuthenticate.ToString().ShouldBe("Bearer");
#pragma warning disable 4014
_fakeExceptionSubscriber
.Received()

@ -5,7 +5,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.Authorization
namespace Volo.Abp.AspNetCore.Mvc
{
public class FakeAuthenticationMiddleware : IMiddleware, ITransientDependency
{

@ -2,10 +2,10 @@
using System.Security.Claims;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.Authorization
namespace Volo.Abp.AspNetCore.Mvc
{
public class FakeUserClaims : ISingletonDependency
{
public List<Claim> Claims { get; } = new List<Claim>();
}
}
}
Loading…
Cancel
Save