MVC Policy Override in Integration Tests - c#

I am in the process of adding integration tests at work for an MVC app. Many of our endpoints have policies applied to them, e.g.
namespace WorkProject
{
[Route("A/Route")]
public class WorkController : Controller
{
[HttpPost("DoStuff")]
[Authorize(Policy = "CanDoStuff")]
public IActionResult DoStuff(){/* */}
}
}
For our integration tests, I have overridden the WebApplicationFactory like it is suggested in the ASP .NET Core documentation. My goal was to overload the authentication step and to bypass the policy by making a class which allows all parties through the authorization policy.
namespace WorkApp.Tests
{
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureServices(services =>
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Test Scheme"; // has to match scheme in TestAuthenticationExtensions
options.DefaultChallengeScheme = "Test Scheme";
}).AddTestAuth(o => { });
services.AddAuthorization(options =>
{
options.AddPolicy("CanDoStuff", policy =>
policy.Requirements.Add(new CanDoStuffRequirement()));
});
// I've also tried the line below, but neither worked
// I figured that maybe the services in Startup were added before these
// and that a replacement was necessary
// services.AddTransient<IAuthorizationHandler, CanDoStuffActionHandler>();
services.Replace(ServiceDescriptor.Transient<IAuthorizationHandler, CanDoStuffActionHandler>());
});
}
}
internal class CanDoStuffActionHandler : AuthorizationHandler<CanDoStuffActionRequirement>
{
public CanDoStuffActionHandler()
{
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CanDoStuffActionRequirement requirement)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
}
internal class CanDoStuffRequirement : IAuthorizationRequirement
{
}
}
The first thing that I do to the services is override the authentication as suggested here (without the bit about overriding Startup since that didn't seem to work for me). I am inclined to believe that this authentication override works. When I run my tests, I receive an HTTP 403 from within the xUnit testing framework. If I hit the route that I am testing from PostMan I receive an HTTP 401. I have also made a class that lives in the custom web application factory that allows all requests for the CanDoStuff authorization handler. I thought this would allow the integration tests through the authorization policy, but, as stated above, I receive an HTTP 403. I know that a 403 will be returned if the app doesn't know where certain files are. However, this is a post route strictly for receiving and processing data and this route does not attempt to return any views so this 403 is most likely related to the authorization policy which, for some reason, is not being overridden.
I'm clearly doing something wrong. When I run the test under debug mode and set a breakpoint in the HandleRequirementsAsync function, the application never breaks. Is there a different way that I should be attempting to override the authorization policies?

Here is what I did.
Override the WebApplicationFactory with my own. Note, I still added my application's startup as the template parameter
Create my on startup function which overrides the ConfigureAuthServices function that I added.
Tell the builder in the ConfigureWebHost function to use my custom startup class.
Override the authentication step in the ConfigureWebHost function via builder.ConfigureServices.
Add an assembly reference to the controller whose endpoint I am trying to hit at the end of builder.ConfigureServices in the ConfigureWebHost function.
Write my own IAuthorizationHandler for the policy that allows all requests to succeed.
I hope I have done a decent job at explaining what I did. If not, hopefully the sample code below is easy enough to follow.
YourController.cs
namespace YourApplication
{
[Route("A/Route")]
public class WorkController : Controller
{
[HttpPost("DoStuff")]
[Authorize(Policy = "CanDoStuff")]
public IActionResult DoStuff(){/* */}
}
}
Test.cs
namespace YourApplication.Tests
{
public class Tests
: IClassFixture<CustomWebApplicationFactory<YourApplication.Startup>>
{
private readonly CustomWebApplicationFactory<YourApplication.Startup> _factory;
public Tests(CustomWebApplicationFactory<YourApplication.Startup> factory)
{
_factory = factory;
}
[Fact]
public async Task SomeTest()
{
var client = _factory.CreateClient();
var response = await client.PostAsync("/YourEndpoint");
response.EnsureSuccessStatusCode();
Assert.Equal(/* whatever your condition is */);
}
}
}
CustomWebApplicationFactory.cs
namespace YourApplication.Tests
{
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureServices(services =>
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Test Scheme"; // has to match scheme in TestAuthenticationExtensions
options.DefaultChallengeScheme = "Test Scheme";
}).AddTestAuth(o => { });
services.AddAuthorization(options =>
{
options.AddPolicy("CanDoStuff", policy =>
policy.Requirements.Add(new CanDoStuffRequirement()));
});
services.AddMvc().AddApplicationPart(typeof(YourApplication.Controllers.YourController).Assembly);
services.AddTransient<IAuthorizationHandler, CanDoStuffActionHandler>();
});
builder.UseStartup<TestStartup>();
}
}
internal class CanDoStuffActionHandler : AuthorizationHandler<CanDoStuffActionRequirement>
{
public CanDoStuffActionHandler()
{
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CanDoStuffActionRequirement requirement)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
}
internal class CanDoStuffRequirement : IAuthorizationRequirement
{
}
}
TestStartup.cs
namespace YourApplication.Tests
{
public class TestStartup : YourApplication.Startup
{
public TestStartup(IConfiguration configuration) : base(configuration)
{
}
protected override void ConfigureAuthServices(IServiceCollection services)
{
}
}
}

Related

Get a service from the builder.Services.AddAuthentication() method

I want to get a registered service from within the AddAuthentication() method but I cannot do so without re-registering all the services again (in BuildServiceProvider).
I get the warning:
"Calling buildserviceprovider from application code results in an additional copy of services."
Is there a way to pass in IServiceCollection? It seems odd it is not already available seeing as I have access to "builder.Services".
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var context = builder.Services.BuildServiceProvider().GetService<IHttpContextAccessor>();
//I want to do this but it's not available.:
options.GetService<IHttpContextAccessor>();
//OR
builder.Services.GetService<IHttpContextAccessor>();
}
First implement IConfigureNamedOptions
public class ConfigurationsJwtBearerOptions : IConfigureNamedOptions<ConfigurationsJwtBearerOptions>
{
IHttpContextAccessor _httpContext;
public ConfigurationsJwtBearerOptions(IHttpContextAccessor httpContext)
{
_httpContext = httpContext;
}
public void Configure(string name, ConfigurationsJwtBearerOptions options)
{
Configure(options);
}
public void Configure(ConfigurationsJwtBearerOptions options)
{
//same code that you usually used in AddJwtBearer (options=>{})
}
}
Then in Progam.cs or StarUp.cs
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.ConfigureOptions<ConfigurationsJwtBearerOptions>().AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();//no need to configurate JwtBearer options here ConfigurationsJwtBearerOptions will handle it

Object deserialization fails POST in Asp Net Core MVC Controller

I created an API app without the full blown MVC template. However, a POST request with body always fails with Internal Server Error.
I followed the exact same steps here for a stateful service:
I created a project with the Stateful Service template in Visual Studio.
I added a Kestrel listener as described here:
protected override IEnumerable<ServiceReplicaListener> CreateServiceReplicaListeners()
{
return new ServiceReplicaListener[]
{
new ServiceReplicaListener(serviceContext =>
new KestrelCommunicationListener(serviceContext, (url, listener) =>
new WebHostBuilder()
.UseKestrel()
.ConfigureServices(
services => services
.AddSingleton<StatefulServiceContext>(serviceContext)
.AddSingleton<IReliableStateManager>(this.StateManager))
.UseContentRoot(Directory.GetCurrentDirectory())
.UseServiceFabricIntegration(listener, ServiceFabricIntegrationOptions.UseUniqueServiceUrl)
.UseStartup<Startup>()
.UseUrls(url)
.Build()
))
};
}
Then, I added a Startup class and a test controller:
internal class Startup
{
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvc();
}
}
[Route("[controller]")]
[ApiController]
public class MyController : ControllerBase
{
[HttpGet("Read1")]
public async Task<ActionResult<string>> Read1()
{
return "asd";
}
[HttpPost("Read2")]
public async Task<ActionResult<string>> Read2()
{
return "asd";
}
[HttpPost("Read3")]
public async Task<ActionResult<MyOutput>> Read([FromBody]MyInput input)
{
var a = new MyOutput();
a.Value= "asdasd";
return a;
}
}
using System.Runtime.Serialization;
[DataContract]
public class MyInput
{
[DataMember]
public string Value{ get; }
public MyInput(string val)
{
Value= val;
}
}
When I make a request to these 3 endpoints, the /Read1 and /Read2 requests work fine. However, /Read3 request fails with Internal Server Error on the client side and I tried to put a breakpoint into controller but for /Read3 it will never hit it. That makes me think the deserialization is not working.
I send the requests from Insomnia. A simple POST request with a content-type header set to "application/json" and JSON body:
{
"Value": "cat"
}
What might be causing this?
In my case, I was missing the json formatter:
services.AddMvcCore().AddJsonFormatters();
Maybe it helps someone.

Authorization Attribute Dependent on Runtime Configuration

I have a .Net Core 3.0 Web API that is configured with as such:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
...
});
services.AddAuthorizationCore(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
And I enable it in the controller like:
[Authorize(Roles = "Admin,Technician")]
public IActionResult CreateFoo([FromBody] Foo foo)
Some api endpoints are also disabled using the [AllowAnonymous].
This product is supporting multiple environments, and one endpoint needs to be either anonymous or authorized dependent on the runtime variable; currently using custom "ASPNETCORE_ENVIRONMENT" options.
I have seen this comment from .net security person, but if I implement a custom policy, it disallows anonymous access.
What is the easiest way to allow anonymous access if the application is running in a certain environment?
If I understand your question then you could create a custom attribute and always grant the user access when the application is running in a certain env?
public class CustomEnvRequirement : AuthorizationHandler<CustomEnvRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomEnvRequirement requirement)
{
string currentEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
// Allow Anonymous when the current env is development.
if (currentEnv.ToLowerInvariant().Equals("development"))
{
context.Succeed(requirement);
}
else if (currentEnv.ToLowerInvariant().Equals("production"))
{
// TODO: add more authorization logic.
}
return Task.CompletedTask;
}
}
And here's the Custom attribute to be added
[Authorize(Policy = "CustomEnv")]
public IActionResult Index()
{
return this.View();
}
Also, make sure to configure it in the startup.cs
services.AddAuthorization(options =>
{
options.AddPolicy("CustomEnv",
policy => policy.Requirements.Add(new CustomEnvRequirement()));
});
AuthorizeAttribute is just an implementation of AuthorizationFilterAttribute . You can create your own implementation that will bypass authentication for certain environments:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class EnvironmentSpecificAutorizeAttribute : AuthorizeAttribute
{
public string AllowAnonymousEnvironment { get; set; }
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
// if currentEnv == AllowAnonymousEnvironment
// return
// else
// base.HandleUnauthorizedRequest(actionContext);
}
public override void OnAuthorization(HttpActionContext actionContext)
{
// same logic as above
base.OnAuthorization(actionContext);
}
public override Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
// same logic as above
return base.OnAuthorizationAsync(actionContext, cancellationToken);
}
}
You may find other suggestions in this thread

Environment dependent controller with [Authorize]

To mark a controller as requiring authorization you typically decorate it like this:
[Authorize]
public class MyController : Controller
Our auth is through a 3rd party provider and given the way it is setup, we only want this to actually be in effect in our production environment, we don't want it to be active in QA environment for example. It's easy to toggle off environment in the Startup.cs file but is there a way to conditionally decorate the controllers? I started looking at policies and roles and that seem like it might be hacked to work but is there a better way?
If you are using Asp.NET Core, Following the documentation here:
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/dependencyinjection?view=aspnetcore-2.1
You can make your custom policy like so:
public class EnvironmentAuthorize : IAuthorizationRequirement
{
public string Environment { get; set; }
public EnvironmentAuthorize(string env)
{
Environment = env;
}
}
public class EnvironmentAuthorizeHandler : AuthorizationHandler<EnvironmentAuthorize>
{
private readonly IHostingEnvironment envionment;
public EnvironmentAuthorizeHandler(IHostingEnvironment env)
{
envionment = env;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EnvironmentAuthorize requirement)
{
if (requirement.Environment != envionment.EnvironmentName)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
In de Startup.cs:
services.AddAuthorization(options =>
{
options.AddPolicy("ProductionOnly", policy =>
policy.Requirements.Add(new EnvironmentAuthorize("Production")));
});
services.AddSingleton<IAuthorizationHandler, EnvironmentAuthorizeHandler>();
In the Controller:
[Authorize(Policy = "ProductionOnly")]
public class MyController : Controller
Although it's possible, i can not recommend this, having different behaviors in different environments is truly a nightmare.

Custom AuthorizationHandler HandleRequirementAsync not called

I can't figure out why my authorization won't succeed.
I found this while looking into potential reasons:
https://github.com/aspnet/Security/issues/1103
Seems like OP had a similar issue, though my issue isn't even related to resource based authorization.
Here's my code:
AuthorizationHandler:
public class DebugOrDeveloperRequirementHandler : AuthorizationHandler<DebugOrDeveloperRequirement>
{
private readonly IHostingEnvironment _environment;
public DebugOrDeveloperRequirementHandler(IHostingEnvironment environment)
{
// breakpoint here - does get hit
_environment = environment;
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DebugOrDeveloperRequirement requirement)
{
// breakpoint here but never hit
if (_environment.IsDevelopment() || _environment.IsIntegrationTest() || context.User.IsInRole(Constants.RoleNames.Developer))
context.Succeed(requirement);
return Task.CompletedTask;
}
}
requirement:
public class DebugOrDeveloperRequirement : IAuthorizationRequirement
{
}
Startup.cs code:
services.AddAuthorization(config =>
{
config.AddPolicy(ApplicationPolicyNames.Contractor, builder =>
{
builder.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireRole(DataLayer.Setup.Constants.RoleNames.Contractor, DataLayer.Setup.Constants.RoleNames.Developer, DataLayer.Setup.Constants.RoleNames.Admin);
});
config.AddPolicy(ApplicationPolicyNames.Customer, builder =>
{
builder.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireRole(DataLayer.Setup.Constants.RoleNames.Customer, DataLayer.Setup.Constants.RoleNames.Developer, DataLayer.Setup.Constants.RoleNames.Admin);
});
config.AddPolicy(ApplicationPolicyNames.Administrator, builder =>
{
builder.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireRole(DataLayer.Setup.Constants.RoleNames.Developer, DataLayer.Setup.Constants.RoleNames.Admin);
});
config.AddPolicy(ApplicationPolicyNames.Developer, builder =>
{
builder.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireRole(DataLayer.Setup.Constants.RoleNames.Developer);
});
config.AddPolicy(ApplicationPolicyNames.DeveloperOrDebug, builder =>
{
builder.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.Requirements.Add(new DebugOrDeveloperRequirement());
});
});
services.AddSingleton<IAuthorizationHandler, DebugOrDeveloperRequirementHandler>();
My code does not look all that different from documentation. Hence i can't really see why this AuthorizationHandler is not called.
Well now i feel silly - i thought action authorize attributes override controller attributes - they don't.
My controller had a Developer Policy - that made the action fail before that handler even got to its execution turn.

Categories