openid connect - identifying tenant during login

后端 未结 3 2057
后悔当初
后悔当初 2020-12-31 21:58

I have a multi-tenant (single database) application which allows for same username/email across different tenants.

At the time of login (Implicit flow) how can I iden

3条回答
  •  独厮守ぢ
    2020-12-31 22:37

    For anyone who's interested in an alternative approach (more of an extension) to Kevin Chalet's accepted answer look at the pattern described here using a custom implementation of IOptions as MultiTenantOptionsManager https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/master/docs/Options.md

    The authentication sample for the same pattern is here https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/master/docs/Authentication.md

    The full source code for the implemenation is here https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/7bc72692b0f509e0348fe17dd3248d35f4f2b52c/src/Finbuckle.MultiTenant.Core/Options/MultiTenantOptionsManager.cs

    The trick is using a custom IOptionsMonitorCache that is tenant aware and always returns a tenant scoped result https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/7bc72692b0f509e0348fe17dd3248d35f4f2b52c/src/Finbuckle.MultiTenant.Core/Options/MultiTenantOptionsCache.cs

        internal class MultiTenantOptionsManager : IOptions, IOptionsSnapshot where TOptions : class, new()
        {
            private readonly IOptionsFactory _factory;
            private readonly IOptionsMonitorCache _cache; // Note: this is a private cache
    
            /// 
            /// Initializes a new instance with the specified options configurations.
            /// 
            /// The factory to use to create options.
            public MultiTenantOptionsManager(IOptionsFactory factory, IOptionsMonitorCache cache)
            {
                _factory = factory;
                _cache = cache;
            }
    
            public TOptions Value
            {
                get
                {
                    return Get(Microsoft.Extensions.Options.Options.DefaultName);
                }
            }
    
            public virtual TOptions Get(string name)
            {
                name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
    
                // Store the options in our instance cache.
                return _cache.GetOrAdd(name, () => _factory.Create(name));
            }
    
            public void Reset()
            {
                _cache.Clear();
            }
        }
    
    public class MultiTenantOptionsCache : IOptionsMonitorCache where TOptions : class
        {
            private readonly IMultiTenantContextAccessor multiTenantContextAccessor;
    
            // The object is just a dummy because there is no ConcurrentSet class.
            //private readonly ConcurrentDictionary> _adjustedOptionsNames =
            //  new ConcurrentDictionary>();
    
            private readonly ConcurrentDictionary> map = new ConcurrentDictionary>();
    
            public MultiTenantOptionsCache(IMultiTenantContextAccessor multiTenantContextAccessor)
            {
                this.multiTenantContextAccessor = multiTenantContextAccessor ?? throw new ArgumentNullException(nameof(multiTenantContextAccessor));
            }
    
            /// 
            /// Clears all cached options for the current tenant.
            /// 
            public void Clear()
            {
                var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
                var cache = map.GetOrAdd(tenantId, new OptionsCache());
    
                cache.Clear();
            }
    
            /// 
            /// Clears all cached options for the given tenant.
            /// 
            /// The Id of the tenant which will have its options cleared.
            public void Clear(string tenantId)
            {
                tenantId = tenantId ?? "";
                var cache = map.GetOrAdd(tenantId, new OptionsCache());
    
                cache.Clear();
            }
    
            /// 
            /// Clears all cached options for all tenants and no tenant.
            /// 
            public void ClearAll()
            {
                foreach(var cache in map.Values)
                    cache.Clear();
            }
    
            /// 
            /// Gets a named options instance for the current tenant, or adds a new instance created with createOptions.
            /// 
            /// The options name.
            /// The factory function for creating the options instance.
            /// The existing or new options instance.
            public TOptions GetOrAdd(string name, Func createOptions)
            {
                if (createOptions == null)
                {
                    throw new ArgumentNullException(nameof(createOptions));
                }
    
                name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
                var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
                var cache = map.GetOrAdd(tenantId, new OptionsCache());
    
                return cache.GetOrAdd(name, createOptions);
            }
    
            /// 
            /// Tries to adds a new option to the cache for the current tenant.
            /// 
            /// The options name.
            /// The options instance.
            /// True if the options was added to the cache for the current tenant.
            public bool TryAdd(string name, TOptions options)
            {
                name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
                var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
                var cache = map.GetOrAdd(tenantId, new OptionsCache());
    
                return cache.TryAdd(name, options);
            }
    
            /// 
            /// Try to remove an options instance for the current tenant.
            /// 
            /// The options name.
            /// True if the options was removed from the cache for the current tenant.
            public bool TryRemove(string name)
            {
                name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
                var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
                var cache = map.GetOrAdd(tenantId, new OptionsCache());
    
                return cache.TryRemove(name);
            }
        }
    
    

    The advantage is you don't have to extend every type of IOption.

    It can be hooked up as shown in the example https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/3c94ab2848758de7c9d0154aeffffd4820dd545fbf/src/Finbuckle.MultiTenant.Core/DependencyInjection/MultiTenantBuilder.cs#L71

            private static MultiTenantOptionsManager BuildOptionsManager(IServiceProvider sp) where TOptions : class, new()
            {
                var cache = ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsCache));
                return (MultiTenantOptionsManager)
                    ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsManager), new[] { cache });
            }
    

    Using it https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/3c94ab2848758de7c9d0154aeffffd4820dd545fbf/src/Finbuckle.MultiTenant.Core/DependencyInjection/MultiTenantBuilder.cs#L43

    
     public static void WithPerTenantOptions(Action tenantInfo) where TOptions : class, new()
       {
               // Other required services likes custom options factory, see the linked example above for full code
    
                Services.TryAddScoped>(sp => BuildOptionsManager(sp));
    
                Services.TryAddSingleton>(sp => BuildOptionsManager(sp));
        }
    
    

    Every time IOptions.Value is called it looks up the multi tenant aware cache to retrieve it. So you can conveniently use it in singletons like the IAuthenticationSchemeProvider as well.

    Now you can register your tenant specific OpenIddictServerOptionsProvider options same as the accepted answer.

提交回复
热议问题