Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing Shared Login using APIs, with identification of tenancy after login #929

Open
GorgonSlayer opened this issue Jan 20, 2025 · 1 comment
Labels

Comments

@GorgonSlayer
Copy link

I am currently attempting to build a multitenant system where a single sign in page (provided by a SPA), is able to perform all the functions of ASP.Net Core Identity. I have some customs requirements due to providing functionality for a mobile app and desktop app. For context, the application is a shift scheduling and time management system.
Requirements:

  • Users need to be able to access multiple tenants after login, selecting the individual tenant preferably. (Default if there is only one tenant connected to the account.
  • A subset of users must be able to self register (administrative users) and a subset must be invited to use the system.
  • Invited Users need to able to switch between tenants if necessary. (think of switching between employers if a user has two roles at two different firms which both use the system for managing their shifts)
  • Certain DbContext operations need to check for data across multiple tenants. (think comparing shifts assigned to users and user availability)

Data Model context:
Users are connected to Tenants (Organisations) via an Employee class. Organisations create an Employee class. Users have a One to Many relationship with Employees.

Below is some of the implemented code:

public class ClockworkDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid>, IMultiTenantDbContext
{
public DbSet<Organisation> Organisations { get; set; }
public DbSet<Employee> Employees { get; set; }

  protected override void OnModelCreating(ModelBuilder modelBuilder)
      {
          //Create the Entities first
          base.OnModelCreating(modelBuilder);
          //Enforcing Multitenancy
          modelBuilder.ConfigureMultiTenant();
      }
public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        this.EnforceMultiTenant();
        return base.SaveChanges(acceptAllChangesOnSuccess);
    }

    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        this.EnforceMultiTenant();
        return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }

public ITenantInfo? TenantInfo { get; }
    public TenantMismatchMode TenantMismatchMode { get; }
    public TenantNotSetMode TenantNotSetMode { get; }
}

MultiTenantStoreDbContext:

public class MultiTenantStoreDbContext : EFCoreStoreDbContext<Organisation>
{
    public MultiTenantStoreDbContext(DbContextOptions options) : base(options)
    {
        
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var configuration = System.Configuration.ConfigurationManager.AppSettings;
        optionsBuilder.UseNpgsql(configuration["DefaultConnection"]);
        base.OnConfiguring(optionsBuilder);
    }
}

ApplicationUser class:

public class ApplicationUser : IdentityUser<Guid>
{
    public DateOnly DateOfBirth { get; set; }//Verification of birthday for identity that this is the correct user.
}

Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddIdentityApiEndpoints<ApplicationUser>()
    .AddEntityFrameworkStores<ClockworkDbContext>();
builder.Services.AddMultiTenant<Organisation>()
    .WithClaimStrategy()
    .WithDistributedCacheStore(TimeSpan.FromHours(72))
    .WithEFCoreStore<MultiTenantStoreDbContext, Organisation>();
var app = builder.Build();
app.MapIdentityApi<ApplicationUser>();
app.UseAuthentication();

At present, when I generate the ApplicationUser class, it is always bound to an Organisation via the Multitenancy system. What I would like to know is what I am doing wrong. I have looked through the past issues, but very few go into details about implementing Multitenancy in this fashion.

@AndrewTriesToCode
Copy link
Contributor

AndrewTriesToCode commented Jan 22, 2025

Hi, thank you for providing god details. If users are one-to-many with a tenant then in most cases effectively it is many-to-many because of course a tenant can have multiple users.

In this case (if you think it makes sense for you) I actually recommend not using Finbuckle's multitenant Identity context but instead use a normal Identity context with a Tenant entity added in and a many-to-many relationship with user. It simplifies some things such as allowing login by email then you can simply pull a list of the users related tenants and give them a UI to switch tenants.

Works well with the session tenant strategy wherein you set a session variable to the tenant they selected then when they click on a tenant. Also works with other strategies such as route or path based where the UI basically links them to the correct uri when they select a tenant from a list. Can work with the claim strategy but you have to basically logout/login the user behind the scenes each time they want to change tenants to reset the claim value.

Then for your other backend application data contexts separate from identity the MultiTenantDbContext can still be useful if you need data separation. For needing to see data across tenants if you are using a shared database I recommend combining IgnoreGlobalQueryFilters alongside a where condition to get the results you need. If using separate database per tenant I recommend separate instances of your context via the factory constructor methods as needed and joining up the results in your code, of course this might not scale well above a few tenants.

I have an older sample that illustrates this and shouldn't be so old that it isn't helpful:
Shared Login Sample aspnetcore2.1

Let me know what you think about that approach. We could also set up a Teams call if you want to talk it out.

Cheers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

No branches or pull requests

2 participants