Environment dependent controller with [Authorize] - c#

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.

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

How to conditionally remove controller from being registered by ASP.NET Core and added to ServiceCollection

We have ASP.NET Core solution with standard Microsoft.Extensions.DependencyInjection and need to register certain control depending on configuration setting.
Some Example ApiController that inherits from ControllerBase and all their related actions should only be registered if certain bool is true.
Is this possible? I looked at services.AddMvc() but I didn't see any option that would easily allow me to either:
Prevent certain ExampleController from being registered
Remove ExampleController and all it's related actions from IServiceCollection after being registered
As pointed out in comments, implement feature filter and register it in your services config:
public class MyFeatureProvider: ControllerFeatureProvider
{
private readonly bool condition;
public MyFeatureProvider(bool condition)
{
this.condition = condition;
}
protected override bool IsController(TypeInfo typeInfo)
{
if (condition && typeInfo.Name == "ExampleController")
{
return false;
}
return base.IsController(typeInfo);
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().ConfigureApplicationPartManager(mgr =>
{
mgr.FeatureProviders.Clear();
mgr.FeatureProviders.Add(new MyFeatureProvider(true));
});
}
}
I'll link the source code in case you'd like to check out stock standard implementation and see how it works for reference

MVC Policy Override in Integration Tests

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)
{
}
}
}

Filter Attribute with Dependency Injection

I'm trying to fully understand Dependency Injections. I'm defining a Filter and would like to read from a configuration file. Is it a better practice to instantiate Configuration inside of the filter or can this be done so globally, such as in the startup? If So, any pointers for how to do so?
public class CompanyFilter : ActionFilterAttribute
{
string _ERPUrl;
public CompanyFilter(IConfiguration iconfiguration)
{
ERPUrl = iconfiguration.GetSection("AppSettings").GetSection("ERPUrl").Value;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.Controller is Controller controller)
controller.ViewBag.ERPUrl = _ERPUrl;
//filterContext.Controller.ViewBag.Company = "Test";
}
}
Startup Class
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
....
Controllers
namespace Projects.Controllers
{
[CompanyFilter]
public class HomeController : Controller
{
....
The following error is produced.
Controllers\HomeController.cs(14,6): error CS7036: There is no argument given that corresponds to the required formal parameter 'iconfiguration' of 'CompanyFilter.CompanyFilter(IConfiguration)'
I would suggest you to use IOptions<T> to retrieve configuration from a file with all of the advantages supported by .Net Core. You can see how to do it here.
Also, to inject it to dependecy injection resolver add services.AddTransient(p => new MyService(mySettings)); to your ConfigureServices() function as transient or scoped or singleton (decide which one suits you better).
If you insist on using IConfiguration to retrieve configuration and solve the problem that you got, you should inject your IConfiguration instance like this services.AddSingleton(Configuration);. Hope this solves your problem.
Based upon some of the feedback here the following is workable by adding to Startup.cs.
services.AddMvc(options => {
options.Filters.Add(new ERPFilter(Configuration));
}
The url can be factored per the point above to improve performance.
url = ...
services.AddMvc(options => {
options.Filters.Add(new ERPFilter(url));
}
To provide an answer based on the comments provided yesterday by others & myself, it is recommended to inject IOptions<T> into your filters, or any other objects which require configuration data to be injected.
You can add your ERP settings to your appSettings.json file like so
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"Erp": {
"Url": "https://localhost"
}
}
To inject your settings into dependencies you must register it via ConfigureServices, you'll also notice that CompanyFilter is added to the IServiceCollection via AddTransient, this is to allow the ServiceFilterAttribute to resolve it at a later stage and inject any dependencies it the filter has.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.Configure<ErpSettings>(Configuration.GetSection("Erp"));
services.AddTransient<CompanyFilter>();
}
To apply your filter on your controller action, use ServiceFilterAttribute(Type)`
[HttpGet]
[ServiceFilter(typeof(CompanyFilter))]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { ViewBag.ERPUrl };
}
In the above code you'll see that I am returning ViewBag.ERPUrl, this is because your ComapnyFilter has overrided OnActionExecuting which is executed before the action is invoked, whereas OnActionExecuted is invoked after your action has finished and before the response is returned to the caller.
This is how theCompanyFilter now looks, you'll notice that the constructor now accepts IOptions<ErpSettings>
public class CompanyFilter : ActionFilterAttribute
{
private readonly ErpSettings erpSettings;
public CompanyFilter(IOptions<ErpSettings> erpSettings)
{
this.erpSettings= erpSettings.Value;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.Controller is Controller controller)
controller.ViewBag.ERPUrl = erpSettings.Url;
}
}
With all of this done, this is the response

ASP.NET Core disable authentication in development environment

Is it possible to "disable" authentication in ASP.NET Core application without changing its logic?
I have a .net website which uses an external identity server app for authentication.
Anyway I would like to be able to mock the authentication when I'm developing it (ASPNETCORE_ENVIRONMENT = Development), airing access to all actions ignoring the authorization attributes.
Is it possible to do it just mocking some services in the service collection?
On updating to net core 3.1, the mvc AllowAnonymousFilter was not working for us any more. We found conditionally adding a custom IAuthorizationHander to be the simplest way forward to conditionally bypass auth.
eg.
/// <summary>
/// This authorisation handler will bypass all requirements
/// </summary>
public class AllowAnonymous : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
foreach (IAuthorizationRequirement requirement in context.PendingRequirements.ToList())
context.Succeed(requirement); //Simply pass all requirements
return Task.CompletedTask;
}
}
Then register this handler conditionally in Startup.ConfigureServices.
private readonly IWebHostEnvironment _env;
public Startup(IWebHostEnvironment env)
{
_env = env;
}
public void ConfigureServices(IServiceCollection services)
{
{...}
//Allows auth to be bypassed
if (_env.IsDevelopment())
services.AddSingleton<IAuthorizationHandler, AllowAnonymous>();
}
Note AddAuthentication and AddAuthorization services are still registered and configured as per prod code (which is nice).
To allow our unit test to bypass auth, we added a new anonymous testbase with a startup class that added this line without any conditions. Nice and simple!
You can bypass authorization in development environment by applying AllowAnonymousAttribute to your endpoints.
Example 1 dotnet new webapi template, .NET 6 (ASP.NET Core 6) and newer
Use AllowAnonymous method in Program.cs to apply AllowAnonymousAttribute to all controllers:
if (app.Environment.IsDevelopment())
app.MapControllers().AllowAnonymous();
else
app.MapControllers();
Example 2 dotnet new webapi template, .NET Core 3.0 - .NET 5 (ASP.NET Core 3.0-5)
Use WithMetadata method in Startup.Configure() to apply AllowAnonymousAttribute to all controllers:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//...
app.UseEndpoints(endpoints =>
{
if (env.IsDevelopment())
endpoints.MapControllers().WithMetadata(new AllowAnonymousAttribute());
else
endpoints.MapControllers();
});
}
Example 3 dotnet new webapi -minimal template, .NET 6 (ASP.NET Core 6) and newer
Use AllowAnonymous method to apply AllowAnonymousAttribute to a minimal API endpoint:
var hiEndpoint = app
.MapGet("/hi", () => "Hello!")
.RequireAuthorization();
if (app.Environment.IsDevelopment())
hiEndpoint.AllowAnonymous();
Details
endpoints and app from the examples above, both implement IEndpointRouteBuilder which has multiple Map extension methods like MapControllers() and MapGet(...) that return IEndpointConventionBuilder.
WithMetadata (available since .NET Core 3.0) and AllowAnonymous (available since .NET 5) are extensions for IEndpointConventionBuilder and can be called upon the results of those Map methods.
AllowAnonymousAttribute's description from the docs:
Specifies that the class or method that this attribute is applied to does not require authorization.
Another solution you may want to consider is using the IPolicyEvaluator. This means that you can keep all the existing security elements.
public class DisableAuthenticationPolicyEvaluator : IPolicyEvaluator
{
public async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
{
// Always pass authentication.
var authenticationTicket = new AuthenticationTicket(new ClaimsPrincipal(), new AuthenticationProperties(), JwtBearerDefaults.AuthenticationScheme);
return await Task.FromResult(AuthenticateResult.Success(authenticationTicket));
}
public async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource)
{
// Always pass authorization
return await Task.FromResult(PolicyAuthorizationResult.Success());
}
}
In the Startup.cs, ensure this appears at the top of the ConfigureServices method. Eg.
public void ConfigureServices(IServiceCollection services)
{
if (env.IsDevelopment())
{
// Disable authentication and authorization.
services.TryAddSingleton<IPolicyEvaluator, DisableAuthenticationPolicyEvaluator>();
}
...
Rather than Startup.cs (and thanks to the comments below) if you are using Core 3.1 and you wish to use the WebApplicationFactory, you can do the following:
public class MyWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
// Disable Authentication.
services.RemoveAll<IPolicyEvaluator>();
services.AddSingleton<IPolicyEvaluator, DisableAuthenticationPolicyEvaluator>();
});
}
}
I've found sollution for this problem on illucIT Blog.
This code must work:
if (env.IsDevelopment()) {
services.AddMvc(opts =>
{
opts.Filters.Add(new AllowAnonymousFilter());
});
} else {
services.AddMvc();
}
It's tricky to give a detailed answer without more details on your end, but I have previously achieved this by conditionally registering:
the external authentication middleware
the global policy that requires an authenticated request
it looked something like:
public class Startup
{
public Startup(IHostingEnvironment env)
{
Environment = env;
}
public IHostingEnvironment Environment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(x =>
{
if (!Environment.IsDevelopment())
{
var authenticatedUserPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
x.Filters.Add(new AuthorizeFilter(authenticatedUserPolicy));
}
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseStaticFiles();
if (!Environment.IsDevelopment())
{
// Register external authentication middleware
}
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
In my case, the authorization filter was applied globally, so every single action of the MVC app required an authenticated user.
If you have different requirements - fine-grained [Authorize] attributes on some actions - then you could probably achieve the same result by changing how the associated authorization policies are built. They could basically contain no requirements at all.
AuthorizationPolicy yourCustomPolicy = null;
if (Environment.IsDevelopment())
{
yourCustomPolicy = new AuthorizationPolicyBuilder().Build();
}
else
{
yourCustomPolicy = new AuthorizationPolicyBuilder()
// chaining appropriate methods to suit your needs
.Build();
}
In ASP.NET Core 6, we managed to disable the authorization without changing any other part from the productive code, just the following logic in Program.cs:
if (!builder.Environment.IsDevelopment())
{
app.MapControllers();
}
else
{
app.MapControllers().AllowAnonymous();
}
This is to clarify #Kirill Lutsenko's answer about the method he found on the IllucIT blog post (note that in my case this is for .NET Core 2.0. I see other answers saying the AllowAnonymousFilter method won't work in .NET Core 3.1):
The Startup class has an overloaded constructor. One of the overloads takes an IHostingEnvironment parameter. You need to use this version of the constructor.
In the Startup class create a property of type IHostingEnvironment. Call it, say, Environment. Then set that property in the constructor.
Then, in the ConfigureServices method, you can use Environment.IsDevelopment().
public class Startup
{
public Startup(IHostingEnvironment environment)
{
Environment = environment;
}
public IHostingEnvironment Environment { get; }
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//...
services.AddMvc(options =>
{
// This uses the Environment property populated in the constructor.
if (Environment.IsDevelopment())
{
options.Filters.Add(new AllowAnonymousFilter());
}
// Set other options here. For example:
options.ModelBinderProviders.Insert(0, new UTCDateTimeModelBinderProvider());
//...
});
//...
}
}
As a side note, in real life we use a different overload of the constructor, which takes both an IConfiguration object and an IHostingEnvironment object as parameters. That allows us to configure services based on an appsettings.json configuration file.
For example:
public class Startup
{
public Startup(IConfiguration configuration, IHostingEnvironment environment)
{
Configuration = configuration;
Environment = environment;
}
public IConfiguration Configuration { get; }
public IHostingEnvironment Environment { get; }
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//...
// Data access via Entity Framework
services.AddDbContext<ContainersDbContext>(options =>
{
options.UseNpgsql(Configuration.GetConnectionString("OrdersDatabase"));
});
//...
}
}

Categories