The Grand Auth Redesign of 2017 #1179

Closed
HaoK opened this Issue Apr 13, 2017 · 38 comments

Comments

Projects
None yet
9 participants
@HaoK
Member

HaoK commented Apr 13, 2017

Latest PRs:

Fixes

To be continued in: #1190

@Eilon @muratg @Tratcher @ajcvickers @blowdart

@RickBlouch

This comment has been minimized.

Show comment
Hide comment
@RickBlouch

RickBlouch Apr 19, 2017

I have read through the history for much of the Auth 2 issues and have browsed the code in the hoak/auth2 branch trying to get an early understanding of the new system. I realize it's still a work in progress so I may be too early and if that's the case just let me know, but since the "Named/multi tenant options #35 " is marked as close I'm hoping you can point me in the right direction.

Right now I am using something very similar to this post where the request pipeline is being forked for each tenant (based on hostname) and I am able to create tenant specific authentication middleware (among other tenant specific middleware setup) configurations based on tenant specific values retrieved from our database. More specifically we have scenarios where some tenants have one or two OIDC middleware configured and others may not have any.

I can see in the new codebase that this will not work the same way with Auth 2 and that's fine, but I couldn't figure out based on reading the various issues and browsing the new codebase exactly how I will replicate the above functionality. Can you point me in the right direction?

I have read through the history for much of the Auth 2 issues and have browsed the code in the hoak/auth2 branch trying to get an early understanding of the new system. I realize it's still a work in progress so I may be too early and if that's the case just let me know, but since the "Named/multi tenant options #35 " is marked as close I'm hoping you can point me in the right direction.

Right now I am using something very similar to this post where the request pipeline is being forked for each tenant (based on hostname) and I am able to create tenant specific authentication middleware (among other tenant specific middleware setup) configurations based on tenant specific values retrieved from our database. More specifically we have scenarios where some tenants have one or two OIDC middleware configured and others may not have any.

I can see in the new codebase that this will not work the same way with Auth 2 and that's fine, but I couldn't figure out based on reading the various issues and browsing the new codebase exactly how I will replicate the above functionality. Can you point me in the right direction?

@HaoK

This comment has been minimized.

Show comment
Hide comment
@HaoK

HaoK Apr 19, 2017

Member

There won't be any way to fork things since there's only a single service collection, but you should be able to register each using the tenant specific name similar to how the cookie name is being set in that post you mention, if you don't know the tenants at startup, can add dynamically schemes via the ISchemeProvider.AddScheme https://github.com/aspnet/HttpAbstractions/blob/dev/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationSchemeProvider.cs#L56:

   services.AddGoogleAuthentication("Google-<tennant>", options => { });

You'll also have to challenge/authenticate using the new tenant specific names as opposed to just always "Google", but I imagine that shouldn't be too big of a change.

Does that sound like it will work for your scenario?

Member

HaoK commented Apr 19, 2017

There won't be any way to fork things since there's only a single service collection, but you should be able to register each using the tenant specific name similar to how the cookie name is being set in that post you mention, if you don't know the tenants at startup, can add dynamically schemes via the ISchemeProvider.AddScheme https://github.com/aspnet/HttpAbstractions/blob/dev/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationSchemeProvider.cs#L56:

   services.AddGoogleAuthentication("Google-<tennant>", options => { });

You'll also have to challenge/authenticate using the new tenant specific names as opposed to just always "Google", but I imagine that shouldn't be too big of a change.

Does that sound like it will work for your scenario?

@CoskunSunali

This comment has been minimized.

Show comment
Hide comment
@CoskunSunali

CoskunSunali Apr 19, 2017

I have a scenario where I implement the things exactly the way @RickBlouch does.

The multiple-schemes solution does not sound like the best scenario - if I don't get it wrong.

The way @RickBlouch and I implement the things makes each tenant able to access only the services specific to itself but the way you suggest by schemes solving the issue requires each tenant to have access to all schemes even if they don't relate to it.

What is more, it requires (if I am not wrong) manually invoking the challenge/authenticate methods when needed as opposed to having a single scheme for each tenant's own pipeline using the same scheme name and having it automatically handled.

@HaoK , I have not really looked through the changes you made but the main complaint about the auth configuration was that we had to configure some of the things too early in the app life cycle. For instance we still did not know which tenant was being requested (lazy initialization) but we had to configure the token endpoint using a specific URL instead of tenant specific services and token endpoints based on the tenant host names.

We discussed the same thing with @PinpointTownes at aspnet-contrib/AspNet.Security.OpenIdConnect.Server#107 shortly. The issue does not provide much information but it should give you an idea. Otherwise I invite @PinpointTownes to this discussion.

The following quotation from @PinpointTownes's comments describes the issue very well, in my opinion.

Sadly, this won't work, as AuthenticationMiddleware calls IOptions<>.Options directly in its constructor (where you can't access the current request, since there's simply none 😄) and stores it as a singleton: https://github.com/aspnet/Security/blob/dev/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs#L26-L34

To support multi-tenancy, AuthenticationMiddleware's inheritors would also have to update their constructor, to avoid accessing the options instance too early. I guess we'd need a ValidateOptions directly in AuthenticationMiddleware to delay options validation.

I have a scenario where I implement the things exactly the way @RickBlouch does.

The multiple-schemes solution does not sound like the best scenario - if I don't get it wrong.

The way @RickBlouch and I implement the things makes each tenant able to access only the services specific to itself but the way you suggest by schemes solving the issue requires each tenant to have access to all schemes even if they don't relate to it.

What is more, it requires (if I am not wrong) manually invoking the challenge/authenticate methods when needed as opposed to having a single scheme for each tenant's own pipeline using the same scheme name and having it automatically handled.

@HaoK , I have not really looked through the changes you made but the main complaint about the auth configuration was that we had to configure some of the things too early in the app life cycle. For instance we still did not know which tenant was being requested (lazy initialization) but we had to configure the token endpoint using a specific URL instead of tenant specific services and token endpoints based on the tenant host names.

We discussed the same thing with @PinpointTownes at aspnet-contrib/AspNet.Security.OpenIdConnect.Server#107 shortly. The issue does not provide much information but it should give you an idea. Otherwise I invite @PinpointTownes to this discussion.

The following quotation from @PinpointTownes's comments describes the issue very well, in my opinion.

Sadly, this won't work, as AuthenticationMiddleware calls IOptions<>.Options directly in its constructor (where you can't access the current request, since there's simply none 😄) and stores it as a singleton: https://github.com/aspnet/Security/blob/dev/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs#L26-L34

To support multi-tenancy, AuthenticationMiddleware's inheritors would also have to update their constructor, to avoid accessing the options instance too early. I guess we'd need a ValidateOptions directly in AuthenticationMiddleware to delay options validation.

@HaoK

This comment has been minimized.

Show comment
Hide comment
@HaoK

HaoK Apr 19, 2017

Member

So far we have only enabled dynamically adding/removing schemes since its is the first building block needed for multi-tenancy.

To keep each tenant's ability to challenge "Google"/"Facebook" with the new stack, you should be able to accomplish this by replacing some combination of IAuthenticationService/IAuthenticationSchemeProvider/IAuthenticationHandlerProvider to resolve the appropriate tenant specific instance using the request.

The main idea was to make auth service based rather than middleware, so there should be a lot more extensibility/flexibility now

Member

HaoK commented Apr 19, 2017

So far we have only enabled dynamically adding/removing schemes since its is the first building block needed for multi-tenancy.

To keep each tenant's ability to challenge "Google"/"Facebook" with the new stack, you should be able to accomplish this by replacing some combination of IAuthenticationService/IAuthenticationSchemeProvider/IAuthenticationHandlerProvider to resolve the appropriate tenant specific instance using the request.

The main idea was to make auth service based rather than middleware, so there should be a lot more extensibility/flexibility now

@RickBlouch

This comment has been minimized.

Show comment
Hide comment
@RickBlouch

RickBlouch Apr 19, 2017

There won't be any way to fork things since there's only a single service collection, but you should be able to register each using the tenant specific name similar to how the cookie name is being set in that post you mention, if you don't know the tenants at startup, can add dynamically schemes via the ISchemeProvider.AddScheme https://github.com/aspnet/HttpAbstractions/blob/dev/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationSchemeProvider.cs#L56:

services.AddGoogleAuthentication("Google-", options => { });

Forking the pipeline was a pretty clean way to do things so I'm sad to see it go; this feels like a step backwards in that regard. From top to bottom, my pipeline could be unique per tenant which was great in terms of isolation. We deal with PCI and HIPAA so the more isolation the better. It's also not realistic to load all tenants at startup time, I can't imagine that would be the case for very many enterprise apps.

Just to confirm what you are suggesting, in order to dynamically add these schemes on the fly, I'm guessing I will have to have a step in my tenant resolution middleware that makes a call to GetSchemeAsync and checks if a scheme exists with my tenant based scheme name "Google-*****". If not, call AddScheme.

Does that sound like what you are anticipating? Will AddScheme be adding additional dependencies to my DI container with every call? At 1000-2000 tenants will that be a problem?

You'll also have to challenge/authenticate using the new tenant specific names as opposed to just always "Google", but I imagine that shouldn't be too big of a change.

I can inject an object that has tenant context so yes this can happen pretty easily, just something extra I need to remember to do that I didn't have to with a forked pipeline.

await _httpContextAccessor.HttpContext.Authentication.SignInAsync("Google-<tenant>", new ClaimsPrincipal(id), authInfo.Properties);

Does that sound like it will work for your scenario?

I don't think I have a good enough understanding of the new way to make that decision yet. I'm going to see if I can evaluate IAuthenticationService/IAuthenticationSchemeProvider/IAuthenticationHandlerProvider next.

There won't be any way to fork things since there's only a single service collection, but you should be able to register each using the tenant specific name similar to how the cookie name is being set in that post you mention, if you don't know the tenants at startup, can add dynamically schemes via the ISchemeProvider.AddScheme https://github.com/aspnet/HttpAbstractions/blob/dev/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationSchemeProvider.cs#L56:

services.AddGoogleAuthentication("Google-", options => { });

Forking the pipeline was a pretty clean way to do things so I'm sad to see it go; this feels like a step backwards in that regard. From top to bottom, my pipeline could be unique per tenant which was great in terms of isolation. We deal with PCI and HIPAA so the more isolation the better. It's also not realistic to load all tenants at startup time, I can't imagine that would be the case for very many enterprise apps.

Just to confirm what you are suggesting, in order to dynamically add these schemes on the fly, I'm guessing I will have to have a step in my tenant resolution middleware that makes a call to GetSchemeAsync and checks if a scheme exists with my tenant based scheme name "Google-*****". If not, call AddScheme.

Does that sound like what you are anticipating? Will AddScheme be adding additional dependencies to my DI container with every call? At 1000-2000 tenants will that be a problem?

You'll also have to challenge/authenticate using the new tenant specific names as opposed to just always "Google", but I imagine that shouldn't be too big of a change.

I can inject an object that has tenant context so yes this can happen pretty easily, just something extra I need to remember to do that I didn't have to with a forked pipeline.

await _httpContextAccessor.HttpContext.Authentication.SignInAsync("Google-<tenant>", new ClaimsPrincipal(id), authInfo.Properties);

Does that sound like it will work for your scenario?

I don't think I have a good enough understanding of the new way to make that decision yet. I'm going to see if I can evaluate IAuthenticationService/IAuthenticationSchemeProvider/IAuthenticationHandlerProvider next.

@HaoK

This comment has been minimized.

Show comment
Hide comment
@HaoK

HaoK Apr 19, 2017

Member

That sounds right at this point, that said, we will likely spend some cycles to have a 'blessed' way that we encourage for multi-tenancy, so things should get better. Make sure to be involved in that issue/PR when we start on that work. cc @blowdart

Member

HaoK commented Apr 19, 2017

That sounds right at this point, that said, we will likely spend some cycles to have a 'blessed' way that we encourage for multi-tenancy, so things should get better. Make sure to be involved in that issue/PR when we start on that work. cc @blowdart

@HaoK

This comment has been minimized.

Show comment
Hide comment
@HaoK

HaoK Apr 19, 2017

Member

Initial wave of PRs pushed to security/MVC/Identity/MusicStore, tests all passed locally so hopefully no 💥

Member

HaoK commented Apr 19, 2017

Initial wave of PRs pushed to security/MVC/Identity/MusicStore, tests all passed locally so hopefully no 💥

@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Apr 19, 2017

Contributor

Otherwise I invite @PinpointTownes to this discussion.

@CoskunSunali haha, thanks!

Multi-tenancy was indeed mentioned a few times during the "Auth 2.0" design: @HaoK was in favor of a "multiple scheme handlers" approach while I was more inclined to think that it was not really scalable and that the best approach would be to have a single scheme handler with potentially an infinity of options, resolved dynamically (per request):
#1113 (comment) #1113 (comment)

While I haven't tested (yet), I believe the approach I mentioned in the ASOS topic should now be possible, since options validation and initialization are now delayed. That said, I don't think the experience is ideal (yet), because a few important things are still missing (e.g async support in the options stack).

I'll start porting ASOS to ASP.NET Core 2.0 in the next few days, so I'll have a chance to test whether things work correctly, even for multi-tenant scenarios 😄

Contributor

PinpointTownes commented Apr 19, 2017

Otherwise I invite @PinpointTownes to this discussion.

@CoskunSunali haha, thanks!

Multi-tenancy was indeed mentioned a few times during the "Auth 2.0" design: @HaoK was in favor of a "multiple scheme handlers" approach while I was more inclined to think that it was not really scalable and that the best approach would be to have a single scheme handler with potentially an infinity of options, resolved dynamically (per request):
#1113 (comment) #1113 (comment)

While I haven't tested (yet), I believe the approach I mentioned in the ASOS topic should now be possible, since options validation and initialization are now delayed. That said, I don't think the experience is ideal (yet), because a few important things are still missing (e.g async support in the options stack).

I'll start porting ASOS to ASP.NET Core 2.0 in the next few days, so I'll have a chance to test whether things work correctly, even for multi-tenant scenarios 😄

@RickBlouch

This comment has been minimized.

Show comment
Hide comment
@RickBlouch

RickBlouch Apr 19, 2017

@PinpointTownes I would be interested to know your findings with the approach you mentioned in that link. I also believe a single scheme approach with options resolved per request is the cleanest option all around. Please let me know where I can follow your progress / results of your testing. I'm not in position right now to be able to play with the 2.0 bits unfortunately.

@PinpointTownes I would be interested to know your findings with the approach you mentioned in that link. I also believe a single scheme approach with options resolved per request is the cleanest option all around. Please let me know where I can follow your progress / results of your testing. I'm not in position right now to be able to play with the 2.0 bits unfortunately.

@HaoK

This comment has been minimized.

Show comment
Hide comment
@HaoK

HaoK Apr 19, 2017

Member

@PinpointTownes I think it should be easier with the new 2.0 stack to implement single handler with infinity options resolved per request if you desire. Its also possible we end up there for the built in auth handlers as well

Member

HaoK commented Apr 19, 2017

@PinpointTownes I think it should be easier with the new 2.0 stack to implement single handler with infinity options resolved per request if you desire. Its also possible we end up there for the built in auth handlers as well

@CoskunSunali

This comment has been minimized.

Show comment
Hide comment
@CoskunSunali

CoskunSunali Apr 19, 2017

@HaoK Do I get that right that you the AuthorizeAttribute of MVC will iterate through the scheme collection and return true as soon as one of the schemes is able to validate the principle/identity?

https://github.com/aspnet/Mvc/pull/6131/files#diff-fbf649a01b0521186bc5d75f07c20d8f

If I am wrong, how do you imagine the AuthorizeAttribute working in a multi-tenant scenario?

Apart from that, does any of the changes you made so far really avoid us using the UsePerTenant<Tenant> implementation we have at #35 (comment) ? If so, can you please be so kind to lead me to the exact file change where I can see the reasons behind the scene?

@HaoK Do I get that right that you the AuthorizeAttribute of MVC will iterate through the scheme collection and return true as soon as one of the schemes is able to validate the principle/identity?

https://github.com/aspnet/Mvc/pull/6131/files#diff-fbf649a01b0521186bc5d75f07c20d8f

If I am wrong, how do you imagine the AuthorizeAttribute working in a multi-tenant scenario?

Apart from that, does any of the changes you made so far really avoid us using the UsePerTenant<Tenant> implementation we have at #35 (comment) ? If so, can you please be so kind to lead me to the exact file change where I can see the reasons behind the scene?

@HaoK

This comment has been minimized.

Show comment
Hide comment
@HaoK

HaoK Apr 19, 2017

Member

Well the long answer for authorize is it is evaluating a policy's requirements against a ClaimsPrincipal, which is assembled using a union of all of the ClaimsPrincipals returned from the schemes specified in the policy.

So for multi-tenant scenarios, the question really is what does the policy look like there, and that depends on how the authentication schemes are setup. If there's a single logical "Google" then the policy can just ask for that. If there's a different scheme per tenant, i.e. "Google-#135", then you likely will have to plug in a tennant-aware AuthorizationPolicyProvider that turns [Authorize("Google")] into really asking for the "Google-#135" scheme.

All that said, revisiting AuthZ is next up, so all of this is subject to change real soon (next few weeks)

The Auth 2.0 changes make the UsePerTenant a bit moot, unless you have a different service container per tenant, since there's a single IAuthenticationSchemeProvider right now.

Member

HaoK commented Apr 19, 2017

Well the long answer for authorize is it is evaluating a policy's requirements against a ClaimsPrincipal, which is assembled using a union of all of the ClaimsPrincipals returned from the schemes specified in the policy.

So for multi-tenant scenarios, the question really is what does the policy look like there, and that depends on how the authentication schemes are setup. If there's a single logical "Google" then the policy can just ask for that. If there's a different scheme per tenant, i.e. "Google-#135", then you likely will have to plug in a tennant-aware AuthorizationPolicyProvider that turns [Authorize("Google")] into really asking for the "Google-#135" scheme.

All that said, revisiting AuthZ is next up, so all of this is subject to change real soon (next few weeks)

The Auth 2.0 changes make the UsePerTenant a bit moot, unless you have a different service container per tenant, since there's a single IAuthenticationSchemeProvider right now.

@CoskunSunali

This comment has been minimized.

Show comment
Hide comment
@CoskunSunali

CoskunSunali Apr 19, 2017

@PinpointTownes, @HaoK

Multi-tenancy was indeed mentioned a few times during the "Auth 2.0" design: @HaoK was in favor of a "multiple scheme handlers" approach while I was more inclined to think that it was not really scalable and that the best approach would be to have a single scheme handler with potentially an infinity of options, resolved dynamically (per request)

I certainly agree. You cannot really initialize the whole tenant instances as part of the first incoming request in an enterprise environment. Imagine having 200 tenants on a server and the server just getting up. Consider the server getting its first request made by a random visitor. Making him wait to initialize 200 tenants all at once and then serving him his request does not sound like a great idea. (I know what @HaoK implemented does not require us to initialize 200 tenants all at once so that is not about what he did/does.)

I definitely don't want my tenants to be able to access the authorization scheme of each other. I would certainly like them to be isolated from each other - the way it should be.

So all in all, if all of these changes are being made, we have to consider scenarios where tenants are dynamically resolved, initialized, re-initialized and even un-initialized at times.

@HaoK, Let me try to give you some concrete examples.

Resolve and initialize a tenant

Happens when a request to that tenant comes in for the first time. Not when you are starting up the Asp.Net app, not when you are configuring the services or the request pipeline. After all of these phases passed. Out of the blue. I might even add a new tenant configuration to my database and it should be ready to be initialized as soon as one makes a request to that tenant's host name.

A tenant - in my humble opinion - is not really a tenant unless it can have its own collection of services and its own service provider (I handle this using some dirty way of cloning the original IServiceCollection and IServiceProvider services). Consider your tenants being able to contain plugins. One tenant might have services related to it registered and others not.

These services should be isolated from each other and sometimes not based on the service. Let's say, if you have a service registered as a singleton, it has cases where it really is a singleton and is shared among the tenant's own service provider. However, there are cases where a singleton is a singleton within the scope of a tenant.

A tenant - again in my humble opinion - is not really a tenant unless it can have its own request pipeline. Again consider your tenants being able to contain plugins. One tenant might have a middleware registered within its request pipeline while others not.

Re-initialize a tenant

Happens when one calls tenant.Reload(), let if be for having a new plugin added to the tenant or even having that tenant's host name configured by the owner of the tenant. Could also be in case one removes a plugin from the tenant. So that the tenant's service collection, service provider and request pipeline gets all created from the scratch.

Technically speaking, re-initializing is basically removing the tenant from a concurrent list of initialized tenants and having it initialized with the first upcoming request.

Un-initialize a tenant

Happens when a tenant is removed from the data store (be it a database or a configuration file) and that should really leave all other tenants intact and should not cause all other tenants to re-initialize.

Technically speaking, un-initializing is basically removing the tenant from a concurrent list of initialized tenants (of course disposing its services, etc for the sake of scaling and performance).

Back to Asp.Net

@HaoK, no pun intended for you. You are at least trying it. Please don't take anything below this line personal. I don't know if it is just me but 80% of the projects I have developed/lead/architected myself in the last 20 years have always required the multi-tenancy scenario. I personally - maybe @PinpointTownes remembers - fought for having Asp.Net Core support the multi-tenancy scenarios even back in 2015. Please see aspnet/Home#743 (comment).

With all these breaking changes - I should now ask those team members to come and cover our times/efforts - and also yours to fix the things that should be well thought initially before the version 1.0 came out.

So many people around me and around the communities complain about the lack of multi-tenancy support in Asp.Net Core framework, also the breaking changes and being have to re-write many things with every release of Asp.Net Core, there is not much to say.

I personally think that the way Asp.Net Core applications are being bootstrapped at the moment is not exactly multi-tenant friendly. @HaoK has no fault in that and my intention is not to point fingers, otherwise I can name a few people who immediately refused my multi-tenancy requests by saying we have other things to do and we will look into that in the future.

I understand the team here develops a "framework" and not a "real app". Meaning, I am aware of the fact that they cannot support all scenarios which developers may or may not need. However, I consider multi-tenancy to be a must have when it comes to a framework using which you develop web apps.

For instance, I don't know who designed it initially, but having the authorization options being have to configured within ConfigureServices was the worst idea ever since invention of computers. We don't have a request, we don't know what host name, no tenant, no service instances, nothing. All you have is the hosting environment, its primitive service provider containing a few services and the logger factory.

Another instance, the built-in service provider. We cannot even create a child container (we can create just a scope) on demand. Even if we hack the hell out of it and somehow create a child container, we cannot add services to or remove from it. Even the constructor of the out of the box ServiceProvider is internal, we always have to use the IServiceProviderFactory which is fine but its Build method does not allow us to provide it with a new collection of child services, etc. I know you can plugin your own IServiceProviderFactory but that requires you to configure one more thing - the IHostBuilder and I think that is a bit of too much. Funny thing is that I read the community asking for child containers but - without pointing finger - some people say we don't see the need for it and still seal classes here and there. Hey, this is a framework we have to extend!

Honestly speaking, I have been developing Asp.Net since version 1 beta 1 (2001?) and I believe I have some idea about what to expect from a framework, I still cannot suggest my clients use Asp.Net Core. It is almost version 2.0 coming out and in my opinion, I still consider it a product in its beta stages. That is mostly because of the incredible amount of breaking changes being announced with every single push to the repo. There is no backwards-compatibility if you update your packages - and you always end-up being have to, to take advantage of a new feature being introduced. Anyone here remembers Microsoft Solutions Framework? MSF states that you should not - and cannot - introduce breaking changes after an RC version is released. Is it just me remembering a million breaking changes being introduced after the RC versions of Asp.Net Core 1.0 were released? Forget about MSF, how in the world a product can get to RC if it is going to have a major re-write of the API and functionality? It is Release Candidate after all.

I think I can sit here and spend my time writing and waste your time reading how important it is for real-world-apps to have multi-tenancy support out of the box for the next 2 days.

To summarize the things in a single sentence: The current implementation of Asp.Net Core does not support multi-tenancy as a first-class citizen and it will not be able to unless the service container and request pipeline can be configured based on a tenant.

When it comes to multi-tenancy in Asp.Net Core, all we are doing here is finding dirty workarounds, unfortunately.

Long live tenants!

@PinpointTownes, @HaoK

Multi-tenancy was indeed mentioned a few times during the "Auth 2.0" design: @HaoK was in favor of a "multiple scheme handlers" approach while I was more inclined to think that it was not really scalable and that the best approach would be to have a single scheme handler with potentially an infinity of options, resolved dynamically (per request)

I certainly agree. You cannot really initialize the whole tenant instances as part of the first incoming request in an enterprise environment. Imagine having 200 tenants on a server and the server just getting up. Consider the server getting its first request made by a random visitor. Making him wait to initialize 200 tenants all at once and then serving him his request does not sound like a great idea. (I know what @HaoK implemented does not require us to initialize 200 tenants all at once so that is not about what he did/does.)

I definitely don't want my tenants to be able to access the authorization scheme of each other. I would certainly like them to be isolated from each other - the way it should be.

So all in all, if all of these changes are being made, we have to consider scenarios where tenants are dynamically resolved, initialized, re-initialized and even un-initialized at times.

@HaoK, Let me try to give you some concrete examples.

Resolve and initialize a tenant

Happens when a request to that tenant comes in for the first time. Not when you are starting up the Asp.Net app, not when you are configuring the services or the request pipeline. After all of these phases passed. Out of the blue. I might even add a new tenant configuration to my database and it should be ready to be initialized as soon as one makes a request to that tenant's host name.

A tenant - in my humble opinion - is not really a tenant unless it can have its own collection of services and its own service provider (I handle this using some dirty way of cloning the original IServiceCollection and IServiceProvider services). Consider your tenants being able to contain plugins. One tenant might have services related to it registered and others not.

These services should be isolated from each other and sometimes not based on the service. Let's say, if you have a service registered as a singleton, it has cases where it really is a singleton and is shared among the tenant's own service provider. However, there are cases where a singleton is a singleton within the scope of a tenant.

A tenant - again in my humble opinion - is not really a tenant unless it can have its own request pipeline. Again consider your tenants being able to contain plugins. One tenant might have a middleware registered within its request pipeline while others not.

Re-initialize a tenant

Happens when one calls tenant.Reload(), let if be for having a new plugin added to the tenant or even having that tenant's host name configured by the owner of the tenant. Could also be in case one removes a plugin from the tenant. So that the tenant's service collection, service provider and request pipeline gets all created from the scratch.

Technically speaking, re-initializing is basically removing the tenant from a concurrent list of initialized tenants and having it initialized with the first upcoming request.

Un-initialize a tenant

Happens when a tenant is removed from the data store (be it a database or a configuration file) and that should really leave all other tenants intact and should not cause all other tenants to re-initialize.

Technically speaking, un-initializing is basically removing the tenant from a concurrent list of initialized tenants (of course disposing its services, etc for the sake of scaling and performance).

Back to Asp.Net

@HaoK, no pun intended for you. You are at least trying it. Please don't take anything below this line personal. I don't know if it is just me but 80% of the projects I have developed/lead/architected myself in the last 20 years have always required the multi-tenancy scenario. I personally - maybe @PinpointTownes remembers - fought for having Asp.Net Core support the multi-tenancy scenarios even back in 2015. Please see aspnet/Home#743 (comment).

With all these breaking changes - I should now ask those team members to come and cover our times/efforts - and also yours to fix the things that should be well thought initially before the version 1.0 came out.

So many people around me and around the communities complain about the lack of multi-tenancy support in Asp.Net Core framework, also the breaking changes and being have to re-write many things with every release of Asp.Net Core, there is not much to say.

I personally think that the way Asp.Net Core applications are being bootstrapped at the moment is not exactly multi-tenant friendly. @HaoK has no fault in that and my intention is not to point fingers, otherwise I can name a few people who immediately refused my multi-tenancy requests by saying we have other things to do and we will look into that in the future.

I understand the team here develops a "framework" and not a "real app". Meaning, I am aware of the fact that they cannot support all scenarios which developers may or may not need. However, I consider multi-tenancy to be a must have when it comes to a framework using which you develop web apps.

For instance, I don't know who designed it initially, but having the authorization options being have to configured within ConfigureServices was the worst idea ever since invention of computers. We don't have a request, we don't know what host name, no tenant, no service instances, nothing. All you have is the hosting environment, its primitive service provider containing a few services and the logger factory.

Another instance, the built-in service provider. We cannot even create a child container (we can create just a scope) on demand. Even if we hack the hell out of it and somehow create a child container, we cannot add services to or remove from it. Even the constructor of the out of the box ServiceProvider is internal, we always have to use the IServiceProviderFactory which is fine but its Build method does not allow us to provide it with a new collection of child services, etc. I know you can plugin your own IServiceProviderFactory but that requires you to configure one more thing - the IHostBuilder and I think that is a bit of too much. Funny thing is that I read the community asking for child containers but - without pointing finger - some people say we don't see the need for it and still seal classes here and there. Hey, this is a framework we have to extend!

Honestly speaking, I have been developing Asp.Net since version 1 beta 1 (2001?) and I believe I have some idea about what to expect from a framework, I still cannot suggest my clients use Asp.Net Core. It is almost version 2.0 coming out and in my opinion, I still consider it a product in its beta stages. That is mostly because of the incredible amount of breaking changes being announced with every single push to the repo. There is no backwards-compatibility if you update your packages - and you always end-up being have to, to take advantage of a new feature being introduced. Anyone here remembers Microsoft Solutions Framework? MSF states that you should not - and cannot - introduce breaking changes after an RC version is released. Is it just me remembering a million breaking changes being introduced after the RC versions of Asp.Net Core 1.0 were released? Forget about MSF, how in the world a product can get to RC if it is going to have a major re-write of the API and functionality? It is Release Candidate after all.

I think I can sit here and spend my time writing and waste your time reading how important it is for real-world-apps to have multi-tenancy support out of the box for the next 2 days.

To summarize the things in a single sentence: The current implementation of Asp.Net Core does not support multi-tenancy as a first-class citizen and it will not be able to unless the service container and request pipeline can be configured based on a tenant.

When it comes to multi-tenancy in Asp.Net Core, all we are doing here is finding dirty workarounds, unfortunately.

Long live tenants!

@blowdart

This comment has been minimized.

Show comment
Hide comment
@blowdart

blowdart Apr 19, 2017

Member

@davidfowl @DamianEdwards so they can have a read, as ideally this goes way beyond identity.

Member

blowdart commented Apr 19, 2017

@davidfowl @DamianEdwards so they can have a read, as ideally this goes way beyond identity.

@blowdart

This comment has been minimized.

Show comment
Hide comment
@blowdart

blowdart Apr 19, 2017

Member

@CoskunSunali So how are you identifying tenants in your setup? Host name? path? Something else. I think part of the problem (aside from Hao being limited to identity here) is that everyone wants different ways to identify tenants, so a list of some concrete ones in use would be a good start.

Member

blowdart commented Apr 19, 2017

@CoskunSunali So how are you identifying tenants in your setup? Host name? path? Something else. I think part of the problem (aside from Hao being limited to identity here) is that everyone wants different ways to identify tenants, so a list of some concrete ones in use would be a good start.

@RickBlouch

This comment has been minimized.

Show comment
Hide comment
@RickBlouch

RickBlouch Apr 19, 2017

@blowdart I can't speak for @CoskunSunali but we are doing hostname tenant resolution. That being said, isn't that just be an implementation detail that should be an abstraction?

You should take a look at SaasKit which cleanly allows for container and pipeline isolation. this sample is pretty simple and shows pipeline forking with a custom tenant resolver that happens to use hostname to identify tenants but could be based on anything in the request.

RickBlouch commented Apr 19, 2017

@blowdart I can't speak for @CoskunSunali but we are doing hostname tenant resolution. That being said, isn't that just be an implementation detail that should be an abstraction?

You should take a look at SaasKit which cleanly allows for container and pipeline isolation. this sample is pretty simple and shows pipeline forking with a custom tenant resolver that happens to use hostname to identify tenants but could be based on anything in the request.

@CoskunSunali

This comment has been minimized.

Show comment
Hide comment
@CoskunSunali

CoskunSunali Apr 19, 2017

@blowdart Thanks for your attention! I can certainly agree that this goes beyond identity and that is exactly why I kept repeating that my words should not be taken personally by @HaoK at all. Should I actually create a new issue in the MVC repo and post my loooong comment as a new issue there?

Regarding your question, it is the host name 100% of the time. I have, personally, never heard of anyone building tenants based on something else. That might be me, I cannot say no one ever would.

Based on my understanding, it does not matter either. The resolution of the tenant is not a part of the framework's job. Tenant is just a concept here and it literally means an entity which corresponds to a request and has its own service container as well as its own request pipeline.

In my case, I find the tenant by its host name (ConcurrentDictionary<string, ITenant>) and pass its own IServiceProvider instance to the RequestServicesFeature of the HTTP context. I also know what middlewares its request pipeline consists of and I invoke them when a request comes into that tenant.

All of the following examples also use host name to resolve the tenant.

https://andrewlock.net/loading-tenants-from-the-database-with-saaskit-in-asp-net-core/
http://benfoster.io/blog/asp-net-5-multitenancy
http://stackoverflow.com/questions/43114075/how-to-implement-multi-tenant-functionality-in-asp-net-core

@blowdart Thanks for your attention! I can certainly agree that this goes beyond identity and that is exactly why I kept repeating that my words should not be taken personally by @HaoK at all. Should I actually create a new issue in the MVC repo and post my loooong comment as a new issue there?

Regarding your question, it is the host name 100% of the time. I have, personally, never heard of anyone building tenants based on something else. That might be me, I cannot say no one ever would.

Based on my understanding, it does not matter either. The resolution of the tenant is not a part of the framework's job. Tenant is just a concept here and it literally means an entity which corresponds to a request and has its own service container as well as its own request pipeline.

In my case, I find the tenant by its host name (ConcurrentDictionary<string, ITenant>) and pass its own IServiceProvider instance to the RequestServicesFeature of the HTTP context. I also know what middlewares its request pipeline consists of and I invoke them when a request comes into that tenant.

All of the following examples also use host name to resolve the tenant.

https://andrewlock.net/loading-tenants-from-the-database-with-saaskit-in-asp-net-core/
http://benfoster.io/blog/asp-net-5-multitenancy
http://stackoverflow.com/questions/43114075/how-to-implement-multi-tenant-functionality-in-asp-net-core

@HaoK

This comment has been minimized.

Show comment
Hide comment
@HaoK

HaoK Apr 19, 2017

Member

I can respond a bit to the auth 2.0 specific pieces, so we definitely are going to support the ability to dynamically add/remove auth schemes, so at a minimum tenants will be able to add/remove specific auth schemes during their load/unload.

But I also wouldn't be surprised if some of the default Auth services would need to be tweaked/replaced as well.

Member

HaoK commented Apr 19, 2017

I can respond a bit to the auth 2.0 specific pieces, so we definitely are going to support the ability to dynamically add/remove auth schemes, so at a minimum tenants will be able to add/remove specific auth schemes during their load/unload.

But I also wouldn't be surprised if some of the default Auth services would need to be tweaked/replaced as well.

@CoskunSunali

This comment has been minimized.

Show comment
Hide comment
@CoskunSunali

CoskunSunali Apr 19, 2017

@RickBlouch and I posted at the same time and wrote the same things.

The resolution of the tenant is just an implementation detail. The problem here is the built in service container and request pipeline being too tied to the startup (bootstrapping?) of the application.

@RickBlouch and I posted at the same time and wrote the same things.

The resolution of the tenant is just an implementation detail. The problem here is the built in service container and request pipeline being too tied to the startup (bootstrapping?) of the application.

@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Apr 20, 2017

Contributor

Please let me know where I can follow your progress / results of your testing.

@RickBlouch sure! I'll post more details here.

@HaoK has no fault in that and my intention is not to point fingers, otherwise I can name a few people who immediately refused my multi-tenancy requests by saying we have other things to do and we will look into that in the future.

@CoskunSunali let me guess... @blowdart ? :trollface:

A tenant - in my humble opinion - is not really a tenant unless it can have its own collection of services and its own service provider (I handle this using some dirty way of cloning the original IServiceCollection and IServiceProvider services). Consider your tenants being able to contain plugins.

What you describe is what I personally call "heavy multi-tenancy". It definitely has interesting pros - it doesn't require supporting multi-tenancy in every component because everything is strongly isolated at the service and middleware level and nothing is shared between tenants - but also cons, as it has a performance impact when you have thousands of tenants up at the same time.

If you like this approach, I really encourage you to take a look at Orchard Core, as it implements multi-tenancy exactly the way you want: tenants are constructed dynamically and can be stopped or restarted separately. Services are not shared between tenants and the tenants pipelines are completely isolated, but you can "import" services defined at the host level when needed (feel free to take a look at Orchard's OpenIddict module to see how we share the data protection services exposed by the host while guaranteeing perfect isolation at the crypto level by spanning tenants-specific sub-protectors: https://github.com/OrchardCMS/Orchard2/blob/master/src/OrchardCore.Modules/Orchard.OpenId/Startup.cs)

For this type of multi-tenancy, I don't think the security stack should do something special (everything should be handled at a higher level, pretty much like what Orchard Core does).

The option I suggested is actually a lot more lightweight, as services would be shared across tenants (only options would be resolved at runtime and would differ between tenants).

@HaoK's approach - registering and resolving as many scheme handlers as needed - was probably somewhere between these two.

Ideally, ASP.NET Core 2.0 should support these 3 approaches, or at least make them "not too painful" to implement.

Contributor

PinpointTownes commented Apr 20, 2017

Please let me know where I can follow your progress / results of your testing.

@RickBlouch sure! I'll post more details here.

@HaoK has no fault in that and my intention is not to point fingers, otherwise I can name a few people who immediately refused my multi-tenancy requests by saying we have other things to do and we will look into that in the future.

@CoskunSunali let me guess... @blowdart ? :trollface:

A tenant - in my humble opinion - is not really a tenant unless it can have its own collection of services and its own service provider (I handle this using some dirty way of cloning the original IServiceCollection and IServiceProvider services). Consider your tenants being able to contain plugins.

What you describe is what I personally call "heavy multi-tenancy". It definitely has interesting pros - it doesn't require supporting multi-tenancy in every component because everything is strongly isolated at the service and middleware level and nothing is shared between tenants - but also cons, as it has a performance impact when you have thousands of tenants up at the same time.

If you like this approach, I really encourage you to take a look at Orchard Core, as it implements multi-tenancy exactly the way you want: tenants are constructed dynamically and can be stopped or restarted separately. Services are not shared between tenants and the tenants pipelines are completely isolated, but you can "import" services defined at the host level when needed (feel free to take a look at Orchard's OpenIddict module to see how we share the data protection services exposed by the host while guaranteeing perfect isolation at the crypto level by spanning tenants-specific sub-protectors: https://github.com/OrchardCMS/Orchard2/blob/master/src/OrchardCore.Modules/Orchard.OpenId/Startup.cs)

For this type of multi-tenancy, I don't think the security stack should do something special (everything should be handled at a higher level, pretty much like what Orchard Core does).

The option I suggested is actually a lot more lightweight, as services would be shared across tenants (only options would be resolved at runtime and would differ between tenants).

@HaoK's approach - registering and resolving as many scheme handlers as needed - was probably somewhere between these two.

Ideally, ASP.NET Core 2.0 should support these 3 approaches, or at least make them "not too painful" to implement.

@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
Contributor

PinpointTownes commented Apr 20, 2017

@blowdart

This comment has been minimized.

Show comment
Hide comment
@blowdart

blowdart Apr 20, 2017

Member

What I want to make clear is the scope here is just identity & security middleware. We're trying to not make things worse in identity & security 2.0, and still enable you to use it to build whatever multi-tenancy scenarios you already have. This is not about making all of ASP.NET Core suitable for multi-tenancy with little to no work on the developer's part. You are not going to get a per tenant pipeline out of this. The ASP.NET Core PMs are aware of that request, but I can't speak to their plans or timelines.

I agree resolution needs to be an implementation detail left to the developer (I've seen tenants resolved by claims in their identity, where people use the generic AAD login page, rather than a tenant specific one for example), it's just nice to get an idea of how you're doing it so we can look at it in mockups and testing.

@PinpointTownes Sometimes you're not helping 😑

Member

blowdart commented Apr 20, 2017

What I want to make clear is the scope here is just identity & security middleware. We're trying to not make things worse in identity & security 2.0, and still enable you to use it to build whatever multi-tenancy scenarios you already have. This is not about making all of ASP.NET Core suitable for multi-tenancy with little to no work on the developer's part. You are not going to get a per tenant pipeline out of this. The ASP.NET Core PMs are aware of that request, but I can't speak to their plans or timelines.

I agree resolution needs to be an implementation detail left to the developer (I've seen tenants resolved by claims in their identity, where people use the generic AAD login page, rather than a tenant specific one for example), it's just nice to get an idea of how you're doing it so we can look at it in mockups and testing.

@PinpointTownes Sometimes you're not helping 😑

@HaoK

This comment has been minimized.

Show comment
Hide comment
@HaoK

HaoK Apr 20, 2017

Member

Well, you don't really mean identity, you mean the Auth (Security repo) stack in AspNet Core :)

Member

HaoK commented Apr 20, 2017

Well, you don't really mean identity, you mean the Auth (Security repo) stack in AspNet Core :)

@blowdart

This comment has been minimized.

Show comment
Hide comment
@blowdart

blowdart Apr 20, 2017

Member

Oops, yea. Editing :)

Member

blowdart commented Apr 20, 2017

Oops, yea. Editing :)

@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Apr 20, 2017

Contributor

@PinpointTownes Sometimes you're not helping 😑

image

Contributor

PinpointTownes commented Apr 20, 2017

@PinpointTownes Sometimes you're not helping 😑

image

@HaoK

This comment has been minimized.

Show comment
Hide comment
@HaoK

HaoK Apr 20, 2017

Member

Ideally, ASP.NET Core 2.0 should support these 3 approaches, or at least make them "not too painful" to implement.

Yup, I agree, the Security stack should support both resolving different options in the same service for different requests, and adding/removing auth services per tenant. Its probably going to be somewhat interesting getting to the point where its "not too painful"...

Member

HaoK commented Apr 20, 2017

Ideally, ASP.NET Core 2.0 should support these 3 approaches, or at least make them "not too painful" to implement.

Yup, I agree, the Security stack should support both resolving different options in the same service for different requests, and adding/removing auth services per tenant. Its probably going to be somewhat interesting getting to the point where its "not too painful"...

@CoskunSunali

This comment has been minimized.

Show comment
Hide comment
@CoskunSunali

CoskunSunali Apr 20, 2017

@PinpointTownes

@CoskunSunali let me guess... @blowdart ? :trollface:

Not pointing any fingers. Past is in the past :)

What you describe is what I personally call "heavy multi-tenancy".

Nice term!

but also cons, as it has a performance impact when you have thousands of tenants up at the same time.

Right, if you don't design it carefully and share what can be shared. The setup I have allows me to share singleton services in between tenants if the instance has nothing to do with the tenant itself. For transient and scoped services, well the service container only contains a reference to the ServiceDescriptor (was it named something else?) anyway. So the pros are greater than cons, e.g.: a few extra object instances in each tenant's service container.

If you like this approach, I really encourage you to take a look at Orchard Core

I have already implemented a similar approach to Orchard's but both have their issues. Take singleton services being resolved while cloning a service provider for instance. Not sure but maybe a Lazy<T> instance could be of some help for that specific issue.

The option I suggested is actually a lot more lightweight, as services would be shared across tenants (only options would be resolved at runtime and would differ between tenants).

I know and I like it. One can use a singleton service and tenants could resolve different options using the same very service instance. And at the same time each tenant can have its own service and its option. It supports implementing it both ways and that is cool!

Ideally, ASP.NET Core 2.0 should support these 3 approaches, or at least make them "not too painful" to implement.

I wish! Even the not too painful would be of great help.

@blowdart

What I want to make clear is the scope here is just identity.

Exactly why I said I could create a separate issue for that on the MVC repo but I guess there are some already.

The ASP.NET Core PMs are aware of that request, but I can't speak to their plans or timelines.

I wish they were involved in here and shared their thoughts on the issue. Then we could plan the future and see if we can depend on the abilities of ASP.NET Core or not.

I've seen tenants resolved by claims in their identity, where people use the generic AAD login page, rather than a tenant specific one for example

I am not sure if I would - personally - call that a tenant resolution but I cannot comment much on that since I don't know the business reasons behind that decision/implementation. In our cases, an anonymous visitor (no claims or what so ever apart from an anonymous identity) can visit a page and his visit should resolve to a tenant using the host name and serve the page. But yes, an implementation detail.

Thanks for your comments, guys.

@PinpointTownes

@CoskunSunali let me guess... @blowdart ? :trollface:

Not pointing any fingers. Past is in the past :)

What you describe is what I personally call "heavy multi-tenancy".

Nice term!

but also cons, as it has a performance impact when you have thousands of tenants up at the same time.

Right, if you don't design it carefully and share what can be shared. The setup I have allows me to share singleton services in between tenants if the instance has nothing to do with the tenant itself. For transient and scoped services, well the service container only contains a reference to the ServiceDescriptor (was it named something else?) anyway. So the pros are greater than cons, e.g.: a few extra object instances in each tenant's service container.

If you like this approach, I really encourage you to take a look at Orchard Core

I have already implemented a similar approach to Orchard's but both have their issues. Take singleton services being resolved while cloning a service provider for instance. Not sure but maybe a Lazy<T> instance could be of some help for that specific issue.

The option I suggested is actually a lot more lightweight, as services would be shared across tenants (only options would be resolved at runtime and would differ between tenants).

I know and I like it. One can use a singleton service and tenants could resolve different options using the same very service instance. And at the same time each tenant can have its own service and its option. It supports implementing it both ways and that is cool!

Ideally, ASP.NET Core 2.0 should support these 3 approaches, or at least make them "not too painful" to implement.

I wish! Even the not too painful would be of great help.

@blowdart

What I want to make clear is the scope here is just identity.

Exactly why I said I could create a separate issue for that on the MVC repo but I guess there are some already.

The ASP.NET Core PMs are aware of that request, but I can't speak to their plans or timelines.

I wish they were involved in here and shared their thoughts on the issue. Then we could plan the future and see if we can depend on the abilities of ASP.NET Core or not.

I've seen tenants resolved by claims in their identity, where people use the generic AAD login page, rather than a tenant specific one for example

I am not sure if I would - personally - call that a tenant resolution but I cannot comment much on that since I don't know the business reasons behind that decision/implementation. In our cases, an anonymous visitor (no claims or what so ever apart from an anonymous identity) can visit a page and his visit should resolve to a tenant using the host name and serve the page. But yes, an implementation detail.

Thanks for your comments, guys.

@DamianEdwards

This comment has been minimized.

Show comment
Hide comment
@DamianEdwards

DamianEdwards Apr 20, 2017

Member

Hi all. Multi-tenancy is a feature that I agree is useful for many customers and applications, and it's an area we intend to investigate for ASP.NET Core as a first-class concern. However, we will do that holistically rather than in a piecemeal fashion across our various components and sub-systems, and it is not in scope for the 2.0 release. That said, where components can make design choices that happen to ease the facilitation of certain aspects of multi-tenancy, without otherwise compromising their own design/function or a later framework-wide feature, we're open to doing that.

Member

DamianEdwards commented Apr 20, 2017

Hi all. Multi-tenancy is a feature that I agree is useful for many customers and applications, and it's an area we intend to investigate for ASP.NET Core as a first-class concern. However, we will do that holistically rather than in a piecemeal fashion across our various components and sub-systems, and it is not in scope for the 2.0 release. That said, where components can make design choices that happen to ease the facilitation of certain aspects of multi-tenancy, without otherwise compromising their own design/function or a later framework-wide feature, we're open to doing that.

@RickBlouch

This comment has been minimized.

Show comment
Hide comment
@RickBlouch

RickBlouch Apr 20, 2017

While I look forward to the day when multi-tenancy is baked into the framework as a whole, it was not my intent with the original post to spawn this into an overall framework issue. I just wanted to understand the go forward strategy that would replace the functionality we're losing in 1.1 specifically with the auth stack. @HaoK and @PinpointTownes have communicated two such possibilities which I will keep an eye on as things progress.

While I look forward to the day when multi-tenancy is baked into the framework as a whole, it was not my intent with the original post to spawn this into an overall framework issue. I just wanted to understand the go forward strategy that would replace the functionality we're losing in 1.1 specifically with the auth stack. @HaoK and @PinpointTownes have communicated two such possibilities which I will keep an eye on as things progress.

@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Apr 20, 2017

Contributor

@CoskunSunali @RickBlouch @HaoK FYI, I just tested the "one scheme, multiple options on demand" multi-tenancy approach with my ASOS 2.0 port and I can confirm it works like a charm, as expected. All you have to do is create a custom IOptionsSnapshot<T> and register it in the DI container:

public class OpenIdConnectServerOptionsSnapshot : IOptionsSnapshot<OpenIdConnectServerOptions>
{
    private readonly IDataProtectionProvider _dataProtectionProvider;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IOptionsCache<OpenIdConnectServerOptions> _optionsCache;

    public OpenIdConnectServerOptionsSnapshot(
        IDataProtectionProvider dataProtectionProvider,
        IHttpContextAccessor httpContextAccessor,
        IOptionsCache<OpenIdConnectServerOptions> optionsCache)
    {
        _dataProtectionProvider = dataProtectionProvider;
        _httpContextAccessor = httpContextAccessor;
        _optionsCache = optionsCache;
    }

    public OpenIdConnectServerOptions Value => Get(null);

    public OpenIdConnectServerOptions Get(string name)
    {
        var tenant = _httpContextAccessor.HttpContext.Request.Host.Value;

        return _optionsCache.GetOrAdd(tenant, () => Create(tenant));
    }

    private OpenIdConnectServerOptions Create(string tenant)
    {
        // Resolve the options associated with the tenant
        // and return the corresponding CLR instance:

        return new OpenIdConnectServerOptions
        {
            ProviderType = typeof(AuthorizationProvider),

            // Enable the authorization, logout, token and userinfo endpoints.
            AuthorizationEndpointPath = "/connect/authorize",
            LogoutEndpointPath = "/connect/logout",
            TokenEndpointPath = "/connect/token",
            UserinfoEndpointPath = "/connect/userinfo",

            // Note: see AuthorizationController.cs for more
            // information concerning ApplicationCanDisplayErrors.
            ApplicationCanDisplayErrors = true,
            AllowInsecureHttp = true,

            // Create a tenant-specific sub-protector to guarantee isolation between tenants.
            DataProtectionProvider = _dataProtectionProvider.CreateProtector(tenant)
        };
    }
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IOptionsSnapshot<OpenIdConnectServerOptions>, OpenIdConnectServerOptionsSnapshot>();
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddScoped<AuthorizationProvider>();

    services.AddAuthentication();

    services.AddOpenIdConnectServer();
}
Contributor

PinpointTownes commented Apr 20, 2017

@CoskunSunali @RickBlouch @HaoK FYI, I just tested the "one scheme, multiple options on demand" multi-tenancy approach with my ASOS 2.0 port and I can confirm it works like a charm, as expected. All you have to do is create a custom IOptionsSnapshot<T> and register it in the DI container:

public class OpenIdConnectServerOptionsSnapshot : IOptionsSnapshot<OpenIdConnectServerOptions>
{
    private readonly IDataProtectionProvider _dataProtectionProvider;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IOptionsCache<OpenIdConnectServerOptions> _optionsCache;

    public OpenIdConnectServerOptionsSnapshot(
        IDataProtectionProvider dataProtectionProvider,
        IHttpContextAccessor httpContextAccessor,
        IOptionsCache<OpenIdConnectServerOptions> optionsCache)
    {
        _dataProtectionProvider = dataProtectionProvider;
        _httpContextAccessor = httpContextAccessor;
        _optionsCache = optionsCache;
    }

    public OpenIdConnectServerOptions Value => Get(null);

    public OpenIdConnectServerOptions Get(string name)
    {
        var tenant = _httpContextAccessor.HttpContext.Request.Host.Value;

        return _optionsCache.GetOrAdd(tenant, () => Create(tenant));
    }

    private OpenIdConnectServerOptions Create(string tenant)
    {
        // Resolve the options associated with the tenant
        // and return the corresponding CLR instance:

        return new OpenIdConnectServerOptions
        {
            ProviderType = typeof(AuthorizationProvider),

            // Enable the authorization, logout, token and userinfo endpoints.
            AuthorizationEndpointPath = "/connect/authorize",
            LogoutEndpointPath = "/connect/logout",
            TokenEndpointPath = "/connect/token",
            UserinfoEndpointPath = "/connect/userinfo",

            // Note: see AuthorizationController.cs for more
            // information concerning ApplicationCanDisplayErrors.
            ApplicationCanDisplayErrors = true,
            AllowInsecureHttp = true,

            // Create a tenant-specific sub-protector to guarantee isolation between tenants.
            DataProtectionProvider = _dataProtectionProvider.CreateProtector(tenant)
        };
    }
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IOptionsSnapshot<OpenIdConnectServerOptions>, OpenIdConnectServerOptionsSnapshot>();
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddScoped<AuthorizationProvider>();

    services.AddAuthentication();

    services.AddOpenIdConnectServer();
}
@CoskunSunali

This comment has been minimized.

Show comment
Hide comment
@CoskunSunali

CoskunSunali Apr 21, 2017

@PinpointTownes Thank you for the code sample! I am sure it will be of great use for me as soon as the new versions of the packages are released!

Cheers.

@PinpointTownes Thank you for the code sample! I am sure it will be of great use for me as soon as the new versions of the packages are released!

Cheers.

@HaoK HaoK referenced this issue in aspnet/Announcements Apr 21, 2017

Closed

Auth 2.0 Breaking Changes #232

@John0King

This comment has been minimized.

Show comment
Hide comment
@John0King

John0King Apr 22, 2017

HttpContext.Authentication will be obsolete

Old:

   context.Authentication.Authenticate|Challenge|SignInAsync("scheme"); // Calls 1.0 auth stack
New:

   using Microsoft.AspNetCore.Authentication;

   context.Authenticate|Challenge|SignInAsync("scheme"); // Calls 2.0 auth stack

I don't like this change, what's the benefit of this Change ?

HttpContext.Authentication will be obsolete

Old:

   context.Authentication.Authenticate|Challenge|SignInAsync("scheme"); // Calls 1.0 auth stack
New:

   using Microsoft.AspNetCore.Authentication;

   context.Authenticate|Challenge|SignInAsync("scheme"); // Calls 2.0 auth stack

I don't like this change, what's the benefit of this Change ?

@HaoK

This comment has been minimized.

Show comment
Hide comment
@HaoK

HaoK Apr 22, 2017

Member

More detailed explanation for the motivation of this change are found in the summary of: #1151

Member

HaoK commented Apr 22, 2017

More detailed explanation for the motivation of this change are found in the summary of: #1151

@HaoK HaoK referenced this issue Apr 27, 2017

Closed

Auth 2.0 Part II: Revenge of AuthZ #1190

9 of 15 tasks complete
@HaoK

This comment has been minimized.

Show comment
Hide comment
@HaoK

HaoK Apr 27, 2017

Member

To be continued: #1190

Member

HaoK commented Apr 27, 2017

To be continued: #1190

@gentledepp

This comment has been minimized.

Show comment
Hide comment
@gentledepp

gentledepp Oct 4, 2017

Will that refactoring make it possible to create authentication providers per tenant and during runtime?
I just read about how you can create separate aspnet pipelines using saaskit here Multi-tenant middleware pipelines in ASP.NET Core

This works, but as far as I understand, you will need to restart the web application, whenever you change any auth configuration, in order for these changes to take effect.
It would be great if we could configure additional login providers without killing the sessions of all other tenants.

Will that refactoring make it possible to create authentication providers per tenant and during runtime?
I just read about how you can create separate aspnet pipelines using saaskit here Multi-tenant middleware pipelines in ASP.NET Core

This works, but as far as I understand, you will need to restart the web application, whenever you change any auth configuration, in order for these changes to take effect.
It would be great if we could configure additional login providers without killing the sessions of all other tenants.

@Tratcher

This comment has been minimized.

Show comment
Hide comment
@Tratcher

Tratcher Oct 4, 2017

Member

Yes, see #1338

Member

Tratcher commented Oct 4, 2017

Yes, see #1338

@gentledepp

This comment has been minimized.

Show comment
Hide comment
@gentledepp

gentledepp Oct 4, 2017

@Tratcher

This comment has been minimized.

Show comment
Hide comment
Member

Tratcher commented Oct 4, 2017

Not yet. aspnet/Docs#4055

@molaie molaie referenced this issue in dazinator/Dotnettency Jan 15, 2018

Closed

ASP.NETcore 2 Support #22

@torangel torangel referenced this issue in IdentityServer/IdentityServer4 Jun 13, 2018

Closed

Multiple External Identity Provider Support #2366

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