I have a few Web API endpoints with no authentication/authorization since it could be used for guest users as well. These endpoints will be consumed directly through XHR/Ajax/JS. However, i would like to allow the request from only a few origins. For this, i've used the Cors middleware like below:
ConfigureServices Method
services.AddCors(options =>
{
options.AddPolicy("AllowSpecific", builder =>
builder.WithOrigins("http://localhost:55476")
.AllowAnyHeader()
.AllowAnyMethod());
});
Configure Method
app.UseCors("AllowSpecific");
This restriction works for requests coming from browsers. However, if the request is coming from Http Clients such as Postman, Fiddler, etc., the request goes through.
Is there any way to tackle such scenarios?
For lack of a better alternative for now, i've replaced CORS middleware with a custom middleware which will check each request's header Origin and allow/restrict based on configuration. This works both for cross-browser requests and HTTP Client requests.
Middleware
public class OriginRestrictionMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private readonly ILogger _logger;
public OriginRestrictionMiddleware(RequestDelegate next, IConfiguration configuration, ILoggerFactory loggerFactory)
{
_next = next;
_configuration = configuration;
_logger = loggerFactory.CreateLogger<OriginRestrictionMiddleware>();
}
public Task Invoke(HttpContext context)
{
try
{
var allowedOriginsConfig = _configuration.GetSection("AllowedOrigins").Value;
var allowedOrigins = allowedOriginsConfig.Split(',');
_logger.LogInformation("Allowed Origins: " + allowedOriginsConfig);
var originHeader = context.Request.Headers.Where(h => h.Key == "Origin");
if (originHeader.Any())
{
var requestOrigin = originHeader.First().Value.ToString();
_logger.LogInformation("Request Origin: " + requestOrigin);
foreach (var origin in allowedOrigins)
{
//if(origin.StartsWith(requestOrigin))
if (requestOrigin.Contains(origin))
{
return _next(context);
}
}
}
context.Response.StatusCode = 401;
return context.Response.WriteAsync("Not Authorized");
}
catch(Exception ex)
{
_logger.LogInformation(ex.ToString());
throw;
}
}
}
public static class OriginRestrictionMiddlewareExtension
{
public static IApplicationBuilder UseOriginRestriction(this IApplicationBuilder builder)
{
return builder.UseMiddleware<OriginRestrictionMiddleware>();
}
}
Startup Configuration
app.UseOriginRestriction();
AppSettings.json
"AllowedOrigins": "http://localhost:55476,http://localhost:55477,chrome-extension"
chrome-extension entry is there to allow request from Postman during development. It will be removed when deployed to server.
I suspect that this solution can also be bypassed one way or another. However, i'm hoping it will work for most of the cases.
Related
We have an .NET 5.0 Web API project with as frontend an Angular project.
In de Web API we use Hangfire to do some jobs. I'm trying to make it work so that our admins can access the hangfire dashboard to be able to check the jobs. So I followed the documentation to do this (https://docs.hangfire.io/en/latest/configuration/using-dashboard.html). The Owin package does not seem to work with our Web API project so I've tried many other options such as added middleware, without Owen, changing the order of UseAuthentication and others.
The problem is that the HttpContext is always mostly empty and so the User is also empty.
As I see it the problem is that it is a Web API and not a MVC project as you see in many online examples. My knowledge of auth is also not that great so any help is welcome!
Some more information:
We use Azure AD as Authentication service
StartUp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, IServiceProvider sp)
{
UseHangfireDashboardCustom(app);
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire");
app.UseAuthentication();
app.UseAuthorization();
}
private static IApplicationBuilder UseHangfireDashboardCustom(IApplicationBuilder app, string pathMatch = "/hangfire", DashboardOptions options = null, JobStorage storage = null)
{
var services = app.ApplicationServices;
storage = storage ?? services.GetRequiredService<JobStorage>();
options = options ?? services.GetService<DashboardOptions>() ?? new DashboardOptions();
var routes = app.ApplicationServices.GetRequiredService<RouteCollection>();
app.Map(new PathString(pathMatch), x =>
x.UseMiddleware<CustomHangfireDashboardMiddleware>(storage, options, routes));
return app;
}
CustomHangfireDashboardMiddleware
public class CustomHangfireDashboardMiddleware
{
private readonly RequestDelegate _nextRequestDelegate;
private readonly JobStorage _jobStorage;
private readonly DashboardOptions _dashboardOptions;
private readonly RouteCollection _routeCollection;
public CustomHangfireDashboardMiddleware(RequestDelegate nextRequestDelegate,
JobStorage storage,
DashboardOptions options,
RouteCollection routes)
{
_nextRequestDelegate = nextRequestDelegate;
_jobStorage = storage;
_dashboardOptions = options;
_routeCollection = routes;
}
public async Task Invoke(HttpContext httpContext)
{
var aspNetCoreDashboardContext = new AspNetCoreDashboardContext(_jobStorage, _dashboardOptions, httpContext);
var findResult = _routeCollection.FindDispatcher(httpContext.Request.Path.Value);
if (findResult == null)
{
await _nextRequestDelegate.Invoke(httpContext);
return;
}
// Attempt to authenticate against Cookies scheme.
// This will attempt to authenticate using data in request, but doesn't send challenge.
var result = await httpContext.AuthenticateAsync();
if (!result.Succeeded)
{
// Request was not authenticated, send challenge and do not continue processing this request.
await httpContext.ChallengeAsync();
return;
}
if (_dashboardOptions.Authorization.Any(filter => filter.Authorize(aspNetCoreDashboardContext) == false))
{
var isAuthenticated = result.Principal?.Identity?.IsAuthenticated ?? false;
if (isAuthenticated == false)
{
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else
{
httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
return;
}
aspNetCoreDashboardContext.UriMatch = findResult.Item2;
await findResult.Item1.Dispatch(aspNetCoreDashboardContext);
}
}
At https://learn.microsoft.com/en-us/azure/app-service/monitor-instances-health-check it is noted that
Large enterprise development teams often need to adhere to security requirements for exposed APIs. To secure the Health check endpoint, you should first use features such as IP restrictions, client certificates, or a Virtual Network to restrict application access. You can secure the Health check endpoint by requiring the User-Agent of the incoming request matches ReadyForRequest/1.0. The User-Agent can't be spoofed since the request would already secured by prior security features.
How could one do this check user-agent in practice? I'm thinking the code
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/health", new HealthCheckOptions { AllowCachingResponses = false });
});
and then in Azure the WebApp would check it's a call originating from Azure service and not from public Internet before replying (and just dropping the call otherwise). I understand there are better ways to do this on the edge, though.
What I think is that the option that came to my mind would be to write a middlware component do check both the URL and agent. Though maybe I miss something obvious and this is not the way? :)
You can create a policy which performs user agent requirement validation
public class UserAgentRequirement : IAuthorizationRequirement
{
public string UserAgent { get; }
public UserAgentRequirement(string userAgent)
{
UserAgent = userAgent;
}
}
public class UserAgentAuthorizationHandler : AuthorizationHandler<UserAgentRequirement>
{
private readonly IHttpContextAccessor httpContextAccessor;
public UserAgentAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserAgentRequirement requirement)
{
var httpContext = httpContextAccessor.HttpContext;
var agent = httpContext.Request.Headers["User-Agent"];
if (agent == requirement.UserAgent)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
Do not forget to register IHttpContextAccessor and UserAgentAuthorizationHandler. In Startup.cs
services.AddHttpContextAccessor();
services.AddScoped<IAuthorizationHandler, UserAgentAuthorizationHandler>();
services.AddAuthorization(options =>
{
//...
options.AddPolicy("HealthCheckPolicy", builder =>
{
builder.AddRequirements(new UserAgentRequirement("ReadyForRequest/1.0"));
});
});
//...
app.UseEndpoints(endpoints =>
{
endpoints
.MapHealthChecks("/health", new HealthCheckOptions { AllowCachingResponses = false })
.RequireAuthorization("HealthCheckPolicy");
//...
});
Hello i am trying to issue a http get request to a .NET Core Console App from my Angular 2 frontend and i get the following error:
Access to XMLHttpRequest at 'http://127.0.0.1:9300/api/getusers' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
For me it is curious since i have enabled CORS on the server side as you can see below in the Startup class.
Startup
public class Startup {
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services) {
services.AddOptions();
services.AddMvc();
}
public IConfiguration Configuration;
public void Configure(IApplicationBuilder app) {
Console.WriteLine("request delievered");
Debug.WriteLine("Entered Server !");
app.UseMvc();
app.UseCors(x => { x.AllowAnyHeader(); x.AllowAnyOrigin();x.AllowAnyMethod(); });
}
}
I make the request from the UI like this:
#Injectable()
export class UserService{
private static baseUrl:string="http://127.0.0.1:9300/api";
constructor(private http:HttpClient) {
}
getClientsAsync():Promise<User[]>{
let route=UserService.baseUrl+"/getusers";
var data=(this.http.get(route) //should i have some headers here?
.map(resp=>resp)
.catch(err=>
Observable.throwError(err)
) as Observable<User[]>).toPromise<User[]>();
return data;
}
}
P.S I have tried with Postman and the request works ,however here in the angular 2 i have not included any headers for my http.get method.Could this be the problem ?
You need to put UseCors before UseMvc.
public void Configure(IApplicationBuilder app) {
Console.WriteLine("request delievered");
Debug.WriteLine("Entered Server !");
app.UseCors(x => { x.AllowAnyHeader(); x.AllowAnyOrigin();x.AllowAnyMethod(); });
app.UseMvc();
}
This is because UseCors adds a middleware (as does UseMvc), and middleware are executed in order from top to bottom. So the request never gets to the CORS middleware.
I have a C# Asp.Net Core (1.x) project, implementing a web REST API, and its related integration test project, where before any test there's a setup similar to:
// ...
IWebHostBuilder webHostBuilder = GetWebHostBuilderSimilarToRealOne()
.UseStartup<MyTestStartup>();
TestServer server = new TestServer(webHostBuilder);
server.BaseAddress = new Uri("http://localhost:5000");
HttpClient client = server.CreateClient();
// ...
During tests, the client is used to send HTTP requests to web API (the system under test) and retrieve responses.
Within actual system under test there's some component extracting sender IP address from each request, as in:
HttpContext httpContext = ReceiveHttpContextDuringAuthentication();
// edge cases omitted for brevity
string remoteIpAddress = httpContext?.Connection?.RemoteIpAddress?.ToString()
Now during integration tests this bit of code fails to find an IP address, as RemoteIpAddress is always null.
Is there a way to set that to some known value from within test code? I searched here on SO but could not find anything similar. TA
You can write middleware to set custom IP Address since this property is writable:
public class FakeRemoteIpAddressMiddleware
{
private readonly RequestDelegate next;
private readonly IPAddress fakeIpAddress = IPAddress.Parse("127.168.1.32");
public FakeRemoteIpAddressMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext httpContext)
{
httpContext.Connection.RemoteIpAddress = fakeIpAddress;
await this.next(httpContext);
}
}
Then you can create StartupStub class like this:
public class StartupStub : Startup
{
public StartupStub(IConfiguration configuration) : base(configuration)
{
}
public override void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware<FakeRemoteIpAddressMiddleware>();
base.Configure(app, env);
}
}
And use it to create a TestServer:
new TestServer(new WebHostBuilder().UseStartup<StartupStub>());
As per this answer in ASP.NET Core, is there any way to set up middleware from Program.cs?
It's also possible to configure the middleware from ConfigureServices, which allows you to create a custom WebApplicationFactory without the need for a StartupStub class:
public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override IWebHostBuilder CreateWebHostBuilder()
{
return WebHost
.CreateDefaultBuilder<Startup>(new string[0])
.ConfigureServices(services =>
{
services.AddSingleton<IStartupFilter, CustomStartupFilter>();
});
}
}
public class CustomStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.UseMiddleware<FakeRemoteIpAddressMiddleware>();
next(app);
};
}
}
Using WebHost.CreateDefaultBuilder can mess up with your app configuration.
And there's no need to change Product code just to accommodate for testing, unless absolutely necessary.
The simplest way to add your own middleware, without overriding Startup class methods, is to add the middleware through a IStartupFilterā as suggested by Elliott's answer.
But instead of using WebHost.CreateDefaultBuilder, just use
base.CreateWebHostBuilder().ConfigureServices...
public class CustomWAF : WebApplicationFactory<Startup>
{
protected override IWebHostBuilder CreateWebHostBuilder()
{
return base.CreateWebHostBuilder().ConfigureServices(services =>
{
services.AddSingleton<IStartupFilter, CustomStartupFilter>();
});
}
}
I used Elliott's answer within an ASP.NET Core 2.2 project. However, updating to ASP.NET 5.0, I had to replace the override of CreateWebHostBuilder with the below override of CreateHostBuilder:
protected override IHostBuilder CreateHostBuilder()
{
return Host
.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
})
.ConfigureServices(services =>
{
services.AddSingleton<IStartupFilter, CustomStartupFilter>();
});
}
I have a domain that has multiple sites underneath it.
In my Startup.cs I have the following code
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
options.Cookies.ApplicationCookie.CookieName = "MyAppName";
options.Cookies.ApplicationCookie.ExpireTimeSpanTimeSpan.FromMinutes(300);
})
.AddEntityFrameworkStores<MyDbContext, Guid>()
.AddDefaultTokenProviders();
On the production machine all my sites are in a subfolder under the same site in IIS so I don't want to use the default domain as the cookie name otherwise different sites cookies will have the same name
What I want is to get the current domain e..g mydomain.com and then append it to an explicit cookiename per site that I specify in Startup.cs e.g.
var domain = "get the server domain here somehow e.g. domain.com";
...
options.Cookies.ApplicationCookie.CookieName = "MyAppName." + domain;
How do I do this?
The reason I ask:
I can see in Chrome developer tools the cookies fall under separate domain names of the site I am accessing. However I sometimes somehow still get the situation of when I log into the same site on two different servers, that I can't log into one and it doesn't log any error. The only solution is to use another browser so I can only assume by the symptoms can only be to do with the cookie.
So my question is really just a personal preference question as in my environment I would prefer to append the domain name to the cookie name although the cookie can only belong to a specific domain.
First of all, i would store domain name in configuration. So it would enable me to change it for current environment.
options.Cookies.ApplicationCookie.CookieName = Configuration["DomainName"];
If you don't like this way you can override cookie option on signin event like this(i am not sure below ways are good):
Events = new CookieAuthenticationEvents()
{
OnSigningIn = async (context) =>
{
context.Options.CookieName = "MyAppName." + context.HttpContext.Request.Host.Value.ToString();
}
}
Or catch first request in configure and override options
bool firstRequest = true;
app.Use(async (context, next) =>
{
if(firstRequest)
{
options.CookieName = "MyAppName." + context.HttpContext.Request.Host.Value.ToString();
firstRequest = false;
}
await next();
});
Also see similar question How to get base url without accessing a request
I found this other way. also I have a blog to documented that.
public class Startup
{
public IConfiguration Configuration { get; }
private WebDomainHelper DomainHelper;
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddScoped(sp => new HttpClient() { BaseAddress = new Uri(DomainHelper.GetDomain()) });
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
DomainHelper = new WebDomainHelper(app.ApplicationServices.GetRequiredService());
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(
endpoints =>
{
endpoints.MapControllerRoute("default", "{controller=Account}/{action=Index}/{id?}");
}
);
}
}
public class WebDomainHelper
{
IHttpContextAccessor ContextAccessor;
public WebDomainHelper(IHttpContextAccessor contextAccessor)
{
ContextAccessor = contextAccessor;
}
///
/// Get domain name
///
public string GetDomain()
{
string serverURL;
try
{
serverURL = $"{ContextAccessor.HttpContext.Request.Scheme}://{ContextAccessor.HttpContext.Request.Host.Value}/";
}
catch
{
serverURL = string.Empty;
}
return serverURL;
}
}