I am currently working on an ASP.NET Core application and I want my application to have multi-tenant capabilities, so I added a TenancyProvider
service that gets injected into my DbContext
(so that I can setup a global query filter later):
public class TenancyProvider : ITenancyProvider
{
private readonly IHttpContextAccessor _context;
public TenancyProvider(IHttpContextAccessor context)
{
_context = context;
}
public string? AuthenticatedUser() => _context.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
My problem now is that this method always returns null. When inspecting the call in debug mode, I saw that the ClaimsPrincipal
does not contain any data, even after login. There are always 0 claims and IsAuthenticated
is always false, so I guess there is a problem with the cookie or reading the data from it. In the Chrome Dev Tools I can see at least that an Identity Cookie exists.
For authentication I use AspNetCore.Identity
. Here's the current code of my login service:
[HttpPost]
public async Task<IActionResult> Login(LoginDTO loginDTO, string? ReturnUrl)
{
if (ModelState.IsValid == false)
{
ViewBag.Errors = ModelState.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage);
return View(loginDTO);
}
var user = await _userManager.FindByEmailAsync(loginDTO.Email);
if (user == null)
{
ModelState.AddModelError("Login", "Ungültiger Benutzer");
return View(loginDTO);
}
var result = await _signInManager.PasswordSignInAsync(loginDTO.Email, loginDTO.Password, isPersistent: false, lockoutOnFailure: false);
if (result.Succeeded)
{
//Return URL Redirect
if (!string.IsNullOrEmpty(ReturnUrl) && Url.IsLocalUrl(ReturnUrl))
{
return LocalRedirect(ReturnUrl);
}
return RedirectToAction("Index", "Orders");
}
ModelState.AddModelError("Login", "Ungültiger Benutzername oder Passwort");
return View(loginDTO);
}
I also tried to add claims manually, but that did not help the situation, so I removed it again.
Here's my Program.cs
:
...
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenancyProvider, TenancyProvider>();
builder.Services.AddDbContext<ApplicationDbContext>(
options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddUserStore<UserStore<ApplicationUser, ApplicationRole, ApplicationDbContext, Guid>>()
.AddRoleStore<RoleStore<ApplicationRole, ApplicationDbContext, Guid>>();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
});
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}
else
{
app.UseDeveloperExceptionPage();
}
app.UseHsts();
app.UseHttpsRedirection();
Rotativa.AspNetCore.RotativaConfiguration.Setup("wwwroot", wkhtmltopdfRelativePath: "Rotativa");
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
//app.MapRazorPages();
app.Run();
I wouldn't be surprised if it is just a small thing that I am missing here, but I have already spent hours now searching the web and trying out different solutions.
Let me know if you need additional info. I am really looking forward to your answers.
Cheers
Update Here's the code where I call the Tenancy Provider in my DB Context:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
{
private readonly string _tenantId;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, ITenancyProvider tenancyProvider) : base(options)
{
_tenantId = tenancyProvider.AuthenticatedUser();
}
...
And here is a screenshot that shows the empty Claims
list and that IsAuthenticated
is false
. The user was already logged in by the time I took the screenshot.
The application behaves like the user is logged in though. I can access Controllers and Pages I cannot access if I am not logged in.
Apparently, the problem was that you cannot access the user from the HttpContext
from the DbContext
constructor. I found a solution that works for my use case here Stackoverflow discussion. More details can also be found in this Github issue.
You need to delay the fetching of the user's information until you actually need them. Therefore, I now just inject the TenancyProvider
service into my constructor. I have also introduced a property that holds the tenant id and the getter calls the TenancyProvider
. And that's basically it.
My DbContext
:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
{
private readonly ITenancyProvider _tenancyProvider;
private ClientID Tenant
{
get
{
string? tenant = _tenancyProvider.AuthenticatedUser();
return tenant.IsNullOrEmpty() ? ClientID.CreateEmpty() : new ClientID(Guid.Parse(tenant));
}
}
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, ITenancyProvider tenancyProvider) : base(options)
{
_tenancyProvider = tenancyProvider;
}
//other members
}
I simply do in my OnModelCreating()
modelBuilder.Entity<Order>()
.HasQueryFilter(x => x.ClientID == Tenant);