Skip to content

An example project with multi tenant hosted Identityserver4

Notifications You must be signed in to change notification settings

navneet10sep/IdsvrMultiTenantExample

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Multi-tenant IdentityServer4 example application

This is an example of how we use IdentityServer4 in a multi-tenant environment.

Why?

I see a lot of questions on how to use IdentityServer4 as an authentication provider in a multi tenant environment. This is a way of showing how we solved this problem and to get feedback from you.

What needed to be solved?

Well, a lot of things actually, although not all related to IdentityServer4 but it needed solving like:

  • how to do multitenancy?
  • how to handle tenant specific IdSvr services?
  • ...

How did we solved it?

Multi tenancy

For this we've used Saaskit.MultiTenancy. This is a great library for creating multi tenant applications in .NetCore.

This is a snippet (Startup.cs) on how we mount the IdentityServer in a tenant aware way (we mount the IdSvr under the path /tenants/<tenant>/)

            app.Map("/tenants", multiTenantIdsvrMountPoint =>
            {
                // Saaskit.Multitenancy
                multiTenantIdsvrMountPoint.UseMultitenancy<IdsvrTenant>();
                multiTenantIdsvrMountPoint.UsePerTenant<IdsvrTenant>((ctx, builder) =>
                {
                    var mountPath = "/" + ctx.Tenant.Name.ToLowerInvariant();
                    // we mount the tenant specific IdSvr4 app under /tenants/<tenant>/
                    builder.Map(mountPath, idsvrForTenantApp =>
                    {
                        var identityServerOptions = idsvrForTenantApp.ApplicationServices.GetRequiredService<IdentityServerOptions>();

                        // we use our own cookie middleware because Idsvr4 expects it to be included in the
                        // pipeline as we have changed the default authentication scheme
                        idsvrForTenantApp.UseCookieAuthentication(new CookieAuthenticationOptions()
                        {
                            AuthenticationScheme = identityServerOptions.AuthenticationOptions.AuthenticationScheme,
                            CookieName = identityServerOptions.AuthenticationOptions.AuthenticationScheme,
                            AutomaticAuthenticate = true,
                            SlidingExpiration = false,
                            ExpireTimeSpan = TimeSpan.FromHours(10)
                        });

                        idsvrForTenantApp.UseIdentityServer();

                        idsvrForTenantApp.UseMvc(routes =>
                        {
                            routes.MapRoute(name: "Account",
                                template: "account/{action=Index}",
                                defaults: new {controller = "Account"});
                        });
                    });
                });
            });

Tenant specific IdSvr services

Some services need to be tenant aware (e.g. ClientStore,...). To solve this, we've used a pattern where a general service will delegate all methods to a tenant specific service.

Example (taken from src/IdsvrMultiTenant/Services/IdSvr/ClientStoreResolver.cs)

    public class ClientStoreResolver : IClientStore
    {
        private IClientStore _clientStoreImplementation;


        public ClientStoreResolver(IHttpContextAccessor httpContextAccessor)
        {
            if(httpContextAccessor == null)
                throw new ArgumentNullException(nameof(httpContextAccessor));

            // just to be sure, we are in a tenant context
            var tenantContext = httpContextAccessor.HttpContext.GetTenantContext<IdsvrTenant>();
            if(tenantContext == null)
                throw new ArgumentNullException(nameof(tenantContext));

            // based on the current tenant, we can redirect to the proper client store
            if (tenantContext.Tenant.Name == "first")
            {
                _clientStoreImplementation = new InMemoryClientStore(GetClientsForFirstClient());
            }
            else if (tenantContext.Tenant.Name == "second")
            {
                _clientStoreImplementation = new InMemoryClientStore(GetClientsForSecondClient());
            }
            else
            {
                // all other tenants have no clients registered in this example
                _clientStoreImplementation = new InMemoryClientStore(new List<Client>());
            }
        }

        public Task<Client> FindClientByIdAsync(string clientId)
        {
            return _clientStoreImplementation.FindClientByIdAsync(clientId);
        }

        private List<Client> GetClientsForFirstClient()
        {
            return new List<Client>
            {
                new Client()
                {
                    ClientId  = "FirstTenantClient",
                    AllowedGrantTypes = new []{ GrantType.AuthorizationCode },
                    RedirectUris = new List<string>()
                    {
                        "http://localhost:5000/signin-oidc"
                    },
                    ClientSecrets = new List<Secret>()
                    {
                        new Secret()
                        {
                            Value = "FirstTenant-ClientSecret".Sha256()
                        }
                    },
                    AllowedScopes = { StandardScopes.OpenId.Name, StandardScopes.Profile.Name },
                    RequireConsent = false,
                    PostLogoutRedirectUris = new List<string>()
                    {
                        "http://localhost:5000/"
                    }
                }
            };
        }

        private List<Client> GetClientsForSecondClient()
        {
            return new List<Client>
            {
                new Client()
                {
                    ClientId  = "SecondTenantClient",
                    AllowedGrantTypes = new []{ GrantType.AuthorizationCode },
                    RedirectUris = new List<string>()
                    {
                        "http://localhost:5001/signin-oidc"
                    },
                    ClientSecrets = new List<Secret>()
                    {
                        new Secret()
                        {
                            Value = "SecondTenant-ClientSecret".Sha256()
                        }
                    },
                    AllowedScopes = { StandardScopes.OpenId.Name, StandardScopes.Profile.Name },
                    RequireConsent = false,
                    PostLogoutRedirectUris = new List<string>()
                    {
                        "http://localhost:5001/"
                    }
                }
            };
        }
    }

What's included?

This example exists of:

In order to run, just execute dotnet run in the three folders.

Your input

For any questions or remarks, please enter a new issue Issues

Feel free to make a pull request for any suggestion you'd have Pull requests

About

An example project with multi tenant hosted Identityserver4

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C# 95.2%
  • CSS 3.6%
  • JavaScript 1.2%