0

How to make this - user1.domain.com goes to user1/index (not inside area) - user2.domain.com goes to user2/index (not inside area)

I mean's the

user1.domain.com/index

user2.domain.com/index

Are same view but different data depending on user{0}

using MVC Core 2.2

  • 1
    This question is pretty unclear. When you say user1.domain.com should go to user1/index, is user1 a controller name and index an action name? Then you said user1.domain.com/index and user2.domain.com index are the same view. This is confusing, because you just said they should go to user1/index and user2/index. If they are the same view, why should they go to something different? Are you actually trying to route to different controllers based on the subdomain, or just parse the user name out of the subdomain? – dangeruss Feb 19 '20 at 22:33
  • I guess the confusing part to me is that I am not sure if you want the request coming in to be redirected/rewritten, OR you just want to get the user{0} from the url and filter data by that without redirecting the incoming requests. If you want the redirection, @itminus's answer is fine, but if you don't want the redirection, then your problem would be just a classic multi-tenant problem, where you can write a middleware to parse the user{0} from the incoming requests, store it somewhere, and then retrieve it when you're generating your views. – David Liang Feb 21 '20 at 00:15

2 Answers2

3

There're several approaches depending on your needs.

How to make this - user1.domain.com goes to user1/index (not inside area) - user2.domain.com goes to user2/index (not inside area)

Rewrite/Redirect

One approach is to rewrite/redirect the url. If you don't like do it with nginx/iis, you could create an Application Level Rewrite Rule. For example, I create a sample route rule for your reference:

internal enum RouteSubDomainBehavior{ Redirect, Rewrite, }
internal class RouteSubDomainRule : IRule
{
    private readonly string _domainWithPort;
    private readonly RouteSubDomainBehavior _behavior;

    public RouteSubDomainRule(string domain, RouteSubDomainBehavior behavior)
    {
        this._domainWithPort = domain;
        this._behavior = behavior;
    }

    // custom this method according to your needs
    protected bool ShouldRewrite(RewriteContext context)
    {
        var req = context.HttpContext.Request;
        // only rewrite the url when it ends with target doamin
        if (!req.Host.Value.EndsWith(this._domainWithPort, StringComparison.OrdinalIgnoreCase)) { return false; }
        // if already rewrite, skip
        if(req.Host.Value.Length == this._domainWithPort.Length) { return false; }
        // ... add other condition to make sure only rewrite for the routes you wish, for example, skip the Hub
        return true;
    }

    public void ApplyRule(RewriteContext context)
    {
        if(!this.ShouldRewrite(context)) { 
            context.Result = RuleResult.ContinueRules; 
            return;
        }
        var req = context.HttpContext.Request;
        if(this._behavior == RouteSubDomainBehavior.Redirect){
            var newUrl = UriHelper.BuildAbsolute( req.Scheme, new HostString(this._domainWithPort), req.PathBase, req.Path, req.QueryString);
            var resp = context.HttpContext.Response;
            context.Logger.LogInformation($"redirect {req.Scheme}://{req.Host}{req.Path}?{req.QueryString} to {newUrl}");
            resp.StatusCode = 301;
            resp.Headers[HeaderNames.Location] = newUrl;
            context.Result = RuleResult.EndResponse;
        }
        else if (this._behavior == RouteSubDomainBehavior.Rewrite)
        {
            var host = req.Host.Value;
            var userStr = req.Host.Value.Substring(0, host.Length - this._domainWithPort.Length - 1);
            req.Host= new HostString(this._domainWithPort);
            var oldPath = req.Path;
            req.Path = $"/{userStr}{oldPath}";
            context.Logger.LogInformation($"rewrite {oldPath} as {req.Path}");
            context.Result = RuleResult.SkipRemainingRules;
        }
        else{
            throw new Exception($"unknow SubDomainBehavoir={this._behavior}");
        }
    }
}

(Note I use Rewrite here. If you like, feel free to change it to RouteSubDomainBehavior.Redirect.)

And then invoke the rewriter middleware just after app.UseStaticFiles():

app.UseStaticFiles();
// note : the invocation order matters!
app.UseRewriter(new RewriteOptions().Add(new RouteSubDomainRule("domain.com:5001",RouteSubDomainBehavior.Rewrite)));

app.UseMvc(...)

By this way,

  • user1.domain.com:5001/ will be rewritten as (or redirected to) domain.com:5001/user1
  • user1.domain.com:5001/Index will be rewritten as(or redirected to) domain.com:5001/user1/Index
  • user1.domain.com:5001/Home/Index will be rewritten as (or redirected to) domain.com:5001/user1//HomeIndex
  • static files like user1.domain.com:5001/lib/jquery/dist/jquery.min.js won't be rewritten/redirected because they're served by UseStaticFiles.

Another Approach Using IModelBinder

Although you can route it by rewritting/redirecting as above, I suspect what your real needs are binding parameters from Request.Host. If that's the case, I would suggest you should use IModelBinder instead. For example, create a new [FromHost] BindingSource:

internal class FromHostAttribute : Attribute, IBindingSourceMetadata
{
    public static readonly BindingSource Instance = new BindingSource( "FromHostBindingSource", "From Host Binding Source", true, true);
    public BindingSource BindingSource {get{ return FromHostAttribute.Instance; }} 
}
public class MyFromHostModelBinder : IModelBinder
{
    private readonly string _domainWithPort;

    public MyFromHostModelBinder()
    {
        this._domainWithPort = "domain.com:5001";  // in real project, use by Configuration/Options
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var req = bindingContext.HttpContext.Request;
        var host = req.Host.Value;
        var name = bindingContext.FieldName;
        var userStr = req.Host.Value.Substring(0, host.Length - this._domainWithPort.Length - 1);
        if (userStr == null) {
            bindingContext.ModelState.AddModelError(name, $"cannot get {name} from Host Domain");
        } else {
            var result = Convert.ChangeType(userStr, bindingContext.ModelType);
            bindingContext.Result = ModelBindingResult.Success(result);
        }
        return Task.CompletedTask;
    }

}
public class FromHostBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) { throw new ArgumentNullException(nameof(context)); }
        var has = context.BindingInfo?.BindingSource == FromHostAttribute.Instance;
        if(has){
            return new BinderTypeModelBinder(typeof(MyFromHostModelBinder));
        }
        return null;
    }
}

Finally, insert this FromHostBinderProvider in your MVC binder providers.

services.AddMvc(otps =>{
    otps.ModelBinderProviders.Insert(0, new FromHostBinderProvider());
});

Now you can get the user1.domain.com automatically by:

public IActionResult Index([FromHost] string username)
{
    ...
    return View(view_model_by_username);
}

public IActionResult Edit([FromHost] string username, string id)
{
    ...
    return View(view_model_by_username);
}
itminus
  • 23,772
  • 2
  • 53
  • 88
  • thank you ... the second approached works prefect and that's exactly what I want. but I do't understand something why if I login with user and go to any sub-domain it's appears I not login ??? I used simple Microsoft login !! Can u explain that ? – Ibrahim Venkat Feb 22 '20 at 10:16
  • @IbrahimVenkat Did you invoke "app.UseAuthentication()" at a very top place? Could you please include the related code or create a new thread on SO? – itminus Feb 24 '20 at 01:16
  • I read more about Share Identity cookie between domain and sub-domain, I found the best solution https://stackoverflow.com/questions/44227873/multiple-subdomains-cookie-in-asp-net-core-identity?rq=1 but I do't found Cookie property !!! options.Cookies.ApplicationCookie.CookieManager = new CookieManager(); //Magic happens here – Ibrahim Venkat Feb 24 '20 at 20:46
0

The problem after login the Identity cookie not shared in sub-domain

enter image description here

Here my Code where's wrong !!!

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    public static Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder dataProtectionBuilder;
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("ConnectionDb")));

        services.AddIdentity<ExtendIdentityUser, IdentityRole>(options =>
        {
            options.Password.RequiredLength = 8;
            options.Password.RequireUppercase = false;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequiredUniqueChars = 0;
            options.Password.RequireLowercase = false;

        }).AddEntityFrameworkStores<ApplicationDbContext>(); // .AddDefaultTokenProviders();

        services.ConfigureApplicationCookie(options => options.CookieManager = new CookieManager());

        services.AddHttpContextAccessor();

        services.AddScoped<IUnitOfWork, UnitOfWork>();
        services.AddScoped<IExtendIdentityUser, ExtendIdentityUserRepository>();
        services.AddScoped<IItems, ItemsRepository>();

        services.AddMvc(otps =>
        {
            otps.ModelBinderProviders.Insert(0, new FromHostBinderProvider());
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
        app.UseStaticFiles();

        app.UseAuthentication();
        //app.UseHttpsRedirection();
        app.UseCookiePolicy();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

And this class to sub-domain like that https://user1.localhost:44390/Home/Index

internal class FromHostAttribute : Attribute, IBindingSourceMetadata
{
    public static readonly BindingSource Instance = new BindingSource("FromHostBindingSource", "From Host Binding Source", true, true);
    public BindingSource BindingSource { get { return FromHostAttribute.Instance; } }
}
public class MyFromHostModelBinder : IModelBinder
{
    private readonly string _domainWithPort;

    public MyFromHostModelBinder()
    {
        this._domainWithPort = "localhost:44390";  // in real project, use by Configuration/Options
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var req = bindingContext.HttpContext.Request;
        var host = req.Host.Value;
        var name = bindingContext.FieldName;
        var userStr = req.Host.Value.Substring(0, host.Length - this._domainWithPort.Length);
        if (string.IsNullOrEmpty(userStr))
        {
            bindingContext.ModelState.AddModelError(name, $"cannot get {name} from Host Domain");
        }
        else
        {
            var result = Convert.ChangeType(userStr, bindingContext.ModelType);
            bindingContext.Result = ModelBindingResult.Success(result);
        }
        return Task.CompletedTask;
    }

}
public class FromHostBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) { throw new ArgumentNullException(nameof(context)); }
        var has = context.BindingInfo?.BindingSource == FromHostAttribute.Instance;
        if (has)
        {
            return new BinderTypeModelBinder(typeof(MyFromHostModelBinder));
        }
        return null;
    }
}
Using ICookieManager 
public class CookieManager : ICookieManager
{
    #region Private Members

    private readonly ICookieManager ConcreteManager;

    #endregion

    #region Prvate Methods

    private string RemoveSubdomain(string host)
    {
        var splitHostname = host.Split('.');
        //if not localhost
        if (splitHostname.Length > 1)
        {
            return string.Join(".", splitHostname.Skip(1));
        }
        else
        {
            return host;
        }
    }

    #endregion

    #region Public Methods

    public CookieManager()
    {
        ConcreteManager = new ChunkingCookieManager();
    }

    public void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)
    {

        options.Domain = RemoveSubdomain(context.Request.Host.Host);  //Set the Cookie Domain using the request from host
        ConcreteManager.AppendResponseCookie(context, key, value, options);
    }

    public void DeleteCookie(HttpContext context, string key, CookieOptions options)
    {
        ConcreteManager.DeleteCookie(context, key, options);
    }

    public string GetRequestCookie(HttpContext context, string key)
    {
        return ConcreteManager.GetRequestCookie(context, key);
    }

    #endregion
}