Search code examples
asp.net-coreasp.net-core-identityclaims

Unable to access the current User Claims after successful Login


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. User seems to be not logged in

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.


Solution

  • 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.

    Here's what you can do:

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