How to get the current domain name in Startup.cs - asp.net-core

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;
}
}

Related

EF Core to call database based on parameter in API

I have an API developed in .NET Core with EF Core. I have to serve multiple clients with different data(but the same schema). This is a school application, where every school want to keep their data separately due to competition etc. So we have a database for each school. Now my challenge is, based on some parameters, I want to change the connection string of my dbContext object.
for e.g., if I call api/students/1 it should get all the students from school 1 and so on. I am not sure whether there is a better method to do it in the configure services itself. But I should be able to pass SchoolId from my client application
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<SchoolDataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("APIConnectionString")));
services.AddScoped<IUnitOfWorkLearn, UnitOfWorkLearn>();
}
11 May 2021
namespace LearnNew
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//Comenting to implement Mr Brownes Solution
//services.AddDbContext<SchoolDataContext>(options =>
// options.UseSqlServer(
// Configuration.GetConnectionString("APIConnectionString")));
services.AddScoped<IUnitOfWorkLearn, UnitOfWorkLearn>();
services.AddControllers();
services.AddHttpContextAccessor();
services.AddDbContext<SchoolDataContext>((sp, options) =>
{
var requestContext = sp.GetRequiredService<HttpContext>();
var constr = GetConnectionStringFromRequestContext(requestContext);
options.UseSqlServer(constr, o => o.UseRelationalNulls());
});
ConfigureSharedKernelServices(services);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "LearnNew", Version = "v1" });
});
}
private string GetConnectionStringFromRequestContext(HttpContext requestContext)
{
//Trying to implement Mr Brownes Solution
var host = requestContext.Request.Host;
// Since I don't know how to get the connection string, I want to
//debug the host variable and see the possible way to get details of
//the host. Below line is temporary until the right method is identified
return Configuration.GetConnectionString("APIConnectionString");
}
private void ConfigureSharedKernelServices(IServiceCollection services)
{
ServiceProvider serviceProvider = services.BuildServiceProvider();
SchoolDataContext appDbContext = serviceProvider.GetService<SchoolDataContext>();
services.RegisterSharedKernel(appDbContext);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "LearnNew v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
You can access the HttpContext when configuring the DbContext like this:
services.AddControllers();
services.AddHttpContextAccessor();
services.AddDbContext<SchoolDataContext>((sp, options) =>
{
var requestContext = sp.GetRequiredService<IHttpContextAccessor>().HttpContext;
var constr = GetConnectionStringFromRequestContext(requestContext);
options.UseSqlServer(constr, o => o.UseRelationalNulls());
});
This code:
var requestContext = sp.GetRequiredService<IHttpContextAccessor>().HttpContext; var constr = GetConnectionStringFromRequestContext(requestContext);
options.UseSqlServer(constr, o => o.UseRelationalNulls());
will run for every request, configuring the connection string based on details from the HttpRequestContext.
If you need to use your DbContext on startup, don't resolve it through DI. Just configure a connection like this:
var ob = new DbContextOptionsBuilder<SchoolDataContext>();
var constr = "...";
ob.UseSqlServer(constr);
using (var db = new Db(ob.Options))
{
db.Database.EnsureCreated();
}
But in production you would normally create all your tenant databases ahead-of-time.

How to check user-agent in ASP.NET Core health check calls (MapHealthChecks)?

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");
//...
});

AspNetCore.Session-Distributed Cache keeps timing out on distributed servers

I have an AspNetCore (core 2.1) web appl that works fine in any single server environment, but times out after a few seconds in the environment with 2 load-balanced web servers.
Here are my startup.cs and other classes, and a screenshot of my AppSessionState table. I hope someone can point me to the right path. I've spent 2 days on this and can't find anything else that needs settings or what's wrong with what I'm doing.
Some explanation of below code:
As seen, I've followed the steps to configure the app to use Distributed SQL Server caching and have a helper static class HttpSessionService which handles adding/getting values from the Session State. Also, I have a Session-Timeout attribute that I annotate each of my controllers to control the session timeouts. And after a few seconds or clicks in the app, as each controller action makes this call
HttpSessionService.Redirect()
this Redirect() method gets a NULL user session from this line, which causes the app to timeout.
var userSession = GetValues<UserIdentityView>(SessionKeys.User);
I've attached two VS debuggers to both servers and I've noticed that even when all sessions coming to one of the debugger instance (one server) the AspNet Session still returned NULL for the above userSession value.
Again, this ONLY happens on a distributed environment, i.e. if I stop one of the sites on one of the web servers everything works fine.
I have looked and implemented the session state distributed caching with SQLServer as explained (the same) in different pages, here are few.
https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-3.0
https://www.c-sharpcorner.com/article/configure-sql-server-session-state-in-asp-net-core/
And I do see sessions being written to my created AppSessionState table, yet the app continues to timeout in the environment with 2 load-balanced servers.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// Session State distributed cache configuration against SQLServer.
var aspStateConnStr = ConfigurationManager.ConnectionStrings["ASPState"].ConnectionString;
var aspSessionStateSchemaName = _config.GetValue<string>("AppSettings:AspSessionStateSchemaName");
var aspSessionStateTbl = _config.GetValue<string>("AppSettings:AspSessionStateTable");
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = aspStateConnStr;
options.SchemaName = aspSessionStateSchemaName;
options.TableName = aspSessionStateTbl;
});
....
services.AddSession(options =>
{
options.IdleTimeout = 1200;
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
...
services.AddMvc().AddJsonOptions(opt => opt.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime, IDistributedCache distCache)
{
var distCacheOptions = new DistributedCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5));
// Session State distributed cache configuration.
lifetime.ApplicationStarted.Register(() =>
{
var currentTimeUTC = DateTime.UtcNow.ToString();
byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
distCache.Set("cachedTimeUTC", encodedCurrentTimeUTC, distCacheOptions);
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSession(); // This must be called before the app.UseMvc()
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
HttpSessionService.Configure(app.ApplicationServices.GetRequiredService<IHttpContextAccessor>(), distCache, distCacheOptions);
}
HttpSessionService (helper class):
public class HttpSessionService
{
private static IHttpContextAccessor _httpContextAccessor;
private static IDistributedCache _distributedCache;
private static ISession _session => _httpContextAccessor.HttpContext.Session;
public static void Configure(IHttpContextAccessor httpContextAccessor, IDistributedCache distCache)
{
_httpContextAccessor = httpContextAccessor;
_distributedCache = distCache;
}
public static void SetValues<T>(string key, T value)
{
_session.Set<T>(key, value);
}
public static T GetValues<T>(string key)
{
var sessionValue = _session.Get<T>(key);
return sessionValue == null ? default(T) : sessionValue;
}
public static bool Redirect()
{
var result = false;
var userSession = GetValues<UserIdentityView>(SessionKeys.User);
if (userSession == null || userSession?.IsAuthenticated == false)
{
result = true;
}
return result;
}
}
SessionTimeoutAttribute:
public class SessionTimeoutAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var redirect = HttpSessionService.Redirect();
if (redirect)
{
context.Result = new RedirectResult("~/Account/SessionTimeOut");
return;
}
base.OnActionExecuting(context);
}
}
MyController
[SessionTimeout]
public class MyController : Controller
{
// Every action in this and any other controller time out and I get redirected by SessionTimeoutAttribute to "~/Account/SessionTimeOut"
}
Sorry for the late reply on this. I've changed my original implementation, by injecting IDistributedCache interface to all of my controllers and using this setting in the Statusup.cs class in ConfigureServices() function.
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = aspStateConnStr;
options.SchemaName = aspSessionStateSchemaName;
options.TableName = aspSessionStateTbl;
options.ExpiredItemsDeletionInterval = null;
});
That made it work in a web farm.
As you can see I'm setting the ExpiredItemsDeletionInterval to null to prevent some basic cache entries from clearing out of cache, but with doing so I ran into another problem that when I attempt to get them I still get null back even if the entry is in the database table. So, that's another thing I'm trying to figure out.
It looks like you're capturing the Session value from HttpContext in your static HttpSessionService instance. That value is per-request so it's definitely going to randomly fail if you capture it like that. You need to go through the IHttpContextAccessor every time you want to access an HttpContext value, if you want to get the latest value.
Also, I'd suggest you pass an HttpContext in to your helper methods rather than using IHttpContextAccessor. It has performance implications and should generally only be used if you absolutely can't pass an HttpContext through. The places you show here seem to have an HttpContext available, so I'd recommend using that instead of the accessor.

Set dummy IP address in integration test with Asp.Net Core TestServer

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>();
});
}

How to protect open/public Web API endpoints

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.