3

We have some OpenID configuration specified in ConfigureServices in Startup.cs:

 services.AddOpenIdConnect("something", "Something", options =>
                {
                    // ... //
                });

How can we change the configuration we've outlined here dynamically, on a per request basis, based on certain rules?

Can this be done in a middleware? If so, please give an example, thank you!

SpiritBob
  • 2,355
  • 3
  • 24
  • 62

2 Answers2

1

Don't think you can do that, but you can if you want add multiple services.AddOpenIdConnect(...) handlers and use a different one for different clients.

What kind of usecase do you have? What do you try to create?

There's nothing stopping you from adding the source of the OpenIdConnectHandler to your own application and then tweaking it to your needs. Its pretty simple and I have done that myself to learn the inner workings of it.

The source is here: https://github.com/dotnet/aspnetcore/tree/master/src/Security/Authentication/OpenIdConnect

Tore Nestenius
  • 16,431
  • 5
  • 30
  • 40
  • Imagine having multi-tenant application which requires every tenant to use a different OpenID connect middle-ware... That's my use-case. Can't we use the events present in the OpenID configuration to change the client's ClientId and ClientSecret property, along with IssuerAddres? I think that's the way to go with this. It allows you to basically achieve what I've been trying to do. – SpiritBob Oct 07 '20 at 23:12
  • tweaked my answer – Tore Nestenius Oct 08 '20 at 06:46
  • Sorry, it didn't answer it. There's a way to achieve what I requested without the necessary need to copy an entire library, which is the reason I can't accept this (as it claims there's no way this can be done, when in fact there is) – SpiritBob Nov 25 '20 at 12:38
  • Cool! if you you like feel free to share how you solved it here, as I am curious :-) – Tore Nestenius Nov 25 '20 at 13:52
  • I used the events provided via the `OpenIdConnectOptions` class, which allow you to take control over the execution of the code at specific parts of the whole authentication flow. I would submit an answer myself, but I'd have to go in gross details of how it would be done, but the short version is basically this ;D Hope it helps! – SpiritBob Nov 25 '20 at 14:27
  • @SpiritBob Hey, I have exactly the same usecase. I need different number of IDPs for each tenant and each one should have own configuration. I have started with implementation similar to https://stackoverflow.com/questions/52955238/how-can-i-set-the-authority-on-openidconnect-middleware-options-dynamically. it allows to have one dynamic OpenIdConnect per tenant. I'm stuck with adding as many as I want providers at the moment. Could you please share some insights? – SerjG Feb 02 '21 at 21:35
  • @SergeyG. hey! I've taken a more drastic approach by injecting directly at the library level what I wanted to do, at the cost of having to occasionally check for any changes in the original library. I basically took a large chunk of their code, and built it to work for my case. But after seeing the post you linked, you should achieve all your goals and needs by utilizing the `OpenIdConnectOptionsInitializer` class. Simply declare your client Id and secrets there, and they should be loaded up automatically. – SpiritBob Feb 03 '21 at 12:37
  • @SergeyG. The code I injected from the library is done via the use of the events I mentioned in my original comment. – SpiritBob Feb 03 '21 at 12:46
  • @SergeyG. after inspecting the code a little bit deeper, basically the magic happens at the `IOptionsMonitor` the author has provided via registering `OpenIdConnectOptionsProvider`. You simply make a call to `OptionsMonitor.CurrentValue`, and inside there a new named option is registered with all the tenant-specific things, which is quite smart. Any tenant-specific configuration, such as client name and client secret and authority should go directly into the `OpenIdConnectOptionsInitializer.Create` method. – SpiritBob Feb 03 '21 at 13:03
  • @SpiritBob thank you for your comments. I think I have done that part. The problem is here: `OpenIdConnectOptionsProvider` has a dictionary `ConcurrentDictionary<(string name, string tenant), Lazy>` which stores `("OpenIdConnect", "tenantId")` as the key and a *single* config as the value. So literally I can register only one OpenIdProvider per tenant. – SerjG Feb 04 '21 at 17:55
  • @SpiritBob I think it's because I have the following config: `services.AddSingleton();` `services.AddSingleton, OpenIdConnectOptionsMonitor>();` `services.AddSingleton, OpenIdConnectOptionsConfigurator>();` `services.AddAuthentication().AddOpenIdConnect();` There is no name or key provided to the `AddOpenIdConnect()` so it will be using default one – SerjG Feb 04 '21 at 17:57
  • @SpiritBob but I need at least one default (or fake) because returning null or `new OpenIdConnectOptions()` leads to the validation issues like `ClientId can't be empty` or so. Also I need to cover different IDPs per tenant: Tenant1 has fake, facebook and google Tenant2 has fake, google and twitter Tenant3 has fake, okta and oauth0 I'm thinking about adding all the possible providers with the `AddOpenIdConnect()` and some fake values and then later configure only certain of them and add code which will be able to distinguish fake from real by the config values. – SerjG Feb 04 '21 at 18:03
  • @SpiritBob so mu question is: was you able to add multiple dynamic providers per tenant or it was single one (like facebook) but with different config per tenant? – SerjG Feb 04 '21 at 18:04
  • One problem as well is that the OpenIDConnect handler uses the ConfigurationManager to download and cache the data from the discovery document and JWKS for 24 hours by default. So you need to use "different" configuration manager instances per tenant, and I doubt that will work. – Tore Nestenius Feb 05 '21 at 07:46
  • @SergeyG. well, my approach to the problem as I said is quite different, so I don't face these problems at all. I have a single OpenIDConnect provider, which is only for **one** specific IDP, for example facebook. When a request is made - it dynamically switches everything. So yes and no - I have one configuration per tenant, because who'd want to have two facebooks per tenant for whatever reason? But back to your question - you simply have to change the dictionary logic, so that you get a unique key not just based on the tenant, but tenant + their provider for example. – SpiritBob Feb 05 '21 at 08:17
  • @SergeyG. that way you'll have yes, a lot of providers, but each one will serve its purpose. I'm just not sure where exactly and how exactly you'd be calling `OptionsMonitor.CurrentValue` now that I think about it. Could you shine some light on this? – SpiritBob Feb 05 '21 at 08:21
  • @SpiritBob I have made it work yesterday. Long story short: 1. In case of missing `tenantId` I just return fake oidc config. 2. Fake oidc options will be filtered out on login page. 3. In case of provided `tenantid` I read all tenant's oidc settings from DB and returning either fake for missing one or configured one for stored. Configs will be saved in the in `_cache` of the `IOptionsMonitor` to prevent future DB accesses for the same tenant. 4. For filtering on the login page I have exposed `_cache` property via another interface in order to access all tenan's configs at a time – SerjG Feb 05 '21 at 09:10
  • @SpiritBobregarding your question about `OptionsMonitor.CurrentValue`: it should not be called because in my implementation it returns `Get(Options.DefaultName)` where `Options.DefaultName` is empty string while should be OIDC name. I was thinking about calling just `IOptionsMonitor.Get()` passing OIDC name but it leads to the foreach loop and calling it for every single OIDC config and acquiring `tenantId` each time inside. So I have exposed `_cache` itself via interface and can just get all OIDC providers for the tenant in order to filter fake ones out – SerjG Feb 05 '21 at 09:15
  • @SergeyG. I'll have to try this in the weekend, see exactly how it works, but I think I understood you? You call `IOptionsMonitor.Get()` in your `LoginController`, but making sure that if a tenant is loaded, you won't load them again? (if for whatever reason their settings were changed, or a provider was added, you won't update them?). And to visualize the buttons for your providers, you return their respective endpoints by utilizing a boundary for `_cache` (i.e an interface)? – SpiritBob Feb 05 '21 at 09:26
  • @SpiritBob. Not exactly. I have IDSRV4. It collects all the IDPs which triggers `IOptionsMonitor.Get(%idp name%)` on preparing login page. I was going to do the same but it's not efficient and I have turned it to custom interface which just exposed `_cache`. Tenant condition is behind of me anyway. Every time I open login page it calls `IOptionsMonitor.Get()` which calls `Configurator` in case `_cache` doesn't have saved config for this tenant. Or it just return fake OIDC config (real object with fake data inside which will never work). Knowing how to identify fake ones I can filter them out. – SerjG Feb 06 '21 at 18:36
  • @SpiritBob `_cache` invalidation is a common question. I'm thinking about adding expiration or a callback (using my interface exposing `_cache`) in order to reread it again. Knowing that each login page access gathers IDPS I can just purge cache and let `Configurator` request it again. Also I would like to limit available IDPs. I don't want to allow tenant to add whatever they want so there is a list of predefined IDPs in the system which tenant may enable by adding their config. It should allow me to enumerate them all in `Startup.cs` and then add configs to the certain once for each tenant – SerjG Feb 06 '21 at 18:41
  • @SerjG Why enumerate them all in Startup? You're loading them up correctly via `IOptionsMonitor.Get(%idp name%)`. You just need to specify which identity providers a tenant can configure on the business level, after that you enumerate all the possible IDP (the ones you've collected basically from your IDSRV4) as you've done! In my specific case I don't know which providers to visualize for my tenant ,because in order to learn what tenant they are - they need to be logged in. (Meaning all tenants login via the same login page, and after they've done so - they are now in tenant mode) – SpiritBob Feb 12 '21 at 09:30
  • @SerjG It's still possible to be implemented, but that's why I've stil not done it as it would require some work on my part and modifications. Basically they'd have to write their e-mail first, and after that I'd be able to render their buttons, as with their e-mail, I can learn what tenant they are. I'd lookup their allowed identity providers in the database, and then for each one on a similar principle invoke `IOptionsMonitor.Get(%idp-name%, tenantId)` (as tenancy for my case has to be decoupled). The allowed identity provider list would then be returned to the front-end for visualization. – SpiritBob Feb 12 '21 at 09:43
  • @SerjG I'd probably also always update the result returned from `IOptionsMonitor.Get(%idp-name%, tenantId)` with the up-to-date information in my database for that specific tenant. `_cache` would only then serve as cache for the actual openId configurations. – SpiritBob Feb 12 '21 at 09:46
  • @SpiritBob I need to enumerate them because asp.net core will call `IOptionsMonitor.Get(%idp name%)` and pass %idp name% only for registered providers in Startup. to identify tenant relation I pass acr_value at the end of the query string in code flow. – SerjG Feb 14 '21 at 21:36
  • @SerjG so i finally went ahead and implemented this alternative approach. I ended up not using the `IOptionsMonitor` implementation, but rather take things even lower by implementing a `IOptionsMonitorCache`, with additions to it being subscribed to an event which can be used to signal that a given configuration should be updated. I'm really not sure if your or mine approach are a good solution to multi-tenancy, because we end up creating a ton of `OpenIdConnectHandler`'s for our tenants. Imagine having 1 million tenants each with 3 providers - you'd have 3 million OpenIdConnectHandlers. – SpiritBob Mar 02 '21 at 15:34
  • @SerjG Each one of those I believe occasionally updates their OpenIdConnectOptions from the `/well-known` endpoint of the authority, which if that's the case, just that is enough of overhead to ditch the whole thing. For that reason I think that directly utilizing just a single `OpenIdConnectHandler` with the idea of dynamically changing the required settings via events to be the better approach, at the cost of you having to maintain parts of the code originally written in the OpenIdConnect nuGet package. – SpiritBob Mar 02 '21 at 15:36
  • @SerjG Curious to hear your thoughts about this. Maybe I'm wrong - maybe having a ton of `OpenIdConnectHandler`'s registered is the way to go and won't actually hinder you, depending on how and when each one refreshes their configuration. – SpiritBob Mar 02 '21 at 15:38
  • @SpiritBob I have local cache of configs stored in dictionary with tenantId as the key. So I init them once and just rtead from the memory. So I bet there is not much diff between using `IOptionsMonitorCache` and my custom cache since both of them should be lazy loaded to the memory in the end. Not sure what do you mean about the handler. I have a single handler per IdP type (one per Facebook, Google, etc). Every time it need to read the data it calls `IOptionsMonitor` which goes to the lazy-loaded mem cache. So I can have 1kk pairs in dictionary (one per tenant) with 3 configs for each tenant – SerjG Mar 02 '21 at 16:22
  • @SerjG ah, I get it. The problem is with me - I'm unable to apply this new approach to my multi-tenancy, because I'm unable to identify or get the current tenant, as the only time I'd be able to know that, is if the user is authenticated, which forced me to load up all the tenants from the database, and for each one to register an authentication handler. – SpiritBob Mar 04 '21 at 07:10
  • @SpiritBob I had the same issue. Then just added additional parameter with tenantId to the auth URL and now I know it. Yes it requires client customization but it should not be a problem. I have added it to Swagger pretty easily – SerjG Mar 04 '21 at 18:19
  • @SerjG using parameters are not an issue. When the app starts, `IOptionsMonitor` is forced to execute and populate the cache. I don't know the current tenant, hence the issues. I can optionally populate the cache with all my tenant's configurations in that first call to the `IOptionsMonitor`, but only one of all those options will be returned from the `Get` method, i.e for all the rest, [the following lines of code won't be executed](https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Core/src/AuthenticationHandler.cs#L142-L143). Am I missing something here? – SpiritBob Mar 04 '21 at 20:27
  • @SpiritBob. Not quite correct. Options will be called in first login, just running the app doesn't trigger the options. I'm using openid code flow and pass tenant Id in the request url. In the options I inject custom tokenid provider which can get tenant id from httpcontext. I think you need to try to put a breakpoint inside the monitor and you will understand cases when it's called – SerjG Mar 07 '21 at 12:29
  • @SerjG are you sure? `IOptionsMonitor` gets invoked at app start - for every registered `OpenIdConnectHandler `in your system. If you have no defined handlers, OIDC won't work, as you'd have no RemoteSchema on which to execute the challenge? I even tested with the simplest of calls - `.AddOpenIdConnect()`. Are you perhaps not defining handlers until the user authenticates? – SpiritBob Mar 08 '21 at 11:47
  • @SpiritBob nom I'm not. Sorry. It's called on the first app access (regardless page). At this point I have no tenant id and return just fake `OpenIdConnectOptions` and `OAuthOptions` objects. Just to pass some basic validation. At this point Idsrv thinks that everything is OK and he will be able to make ext auth if needed. It calls it once per available ext IdP type (twitter, FB, google) – SerjG Mar 08 '21 at 19:16
  • @SpiritBob on `Account/Login` access it calls it again. it calls it each time I access Login page. I think the reason is because it's a monitor and not just config. So it looks for new version each time. If it's just `Account/Login` direct access without tenant passing I return same fake config objects. Inside the Login action I have several conditions and filter so I hide buttons for providers which are actually fake (I can determine fake by anchors in the options value, I literally put "fake" word to all required fields). So at this point login view shows only local account: login + pass – SerjG Mar 08 '21 at 19:21
  • @SpiritBob on code flow login I pass `tenantId` parameter so I know the tenant Id and can make a request to the API in order to get correct IdP config. I put such config in the cache in order to not request them many times. And I return correct Options value here. So I can show Login view with correct configured IdPs only. Even if tenant has FB only then FB will have correct one which will be shown while Twitter and Google will have fake values and will be filtered out. At this point I know correct IdP config for code flow passing tenantId so I can login using tenant specific ext IdP config – SerjG Mar 08 '21 at 19:25
0
            if (await _authenticationSchemeProvider.GetSchemeAsync(OpenIdConnectDefaults.AuthenticationScheme) == null)
                _authenticationSchemeProvider.AddScheme(new AuthenticationScheme(OpenIdConnectDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme, typeof(OpenIdConnectHandler)));

            var opendIDoption = new OpenIdConnectOptions();
            OpenIDSSOConfiguration.SetupOIDOption(opendIDoption, openIDSSOConfiguration);
            foreach(var postConfigure in _postConfigures)
            {
                postConfigure.PostConfigure(OpenIdConnectDefaults.AuthenticationScheme, opendIDoption);
            }

            _optionsCache.TryRemove(OpenIdConnectDefaults.AuthenticationScheme);
            _optionsCache.TryAdd(OpenIdConnectDefaults.AuthenticationScheme, opendIDoption);
            _postConfigureOptions.PostConfigure(OpenIdConnectDefaults.AuthenticationScheme, opendIDoption);

Here is my working solution of my. serwices used are:

IEnumerable<IPostConfigureOptions<OpenIdConnectOptions>> postConfigures,
IPostConfigureOptions<OpenIdConnectOptions> postConfigureOptions, 
IOptionsMonitorCache<OpenIdConnectOptions> optionsCache, 
IAuthenticationSchemeProvider authenticationSchemeProvider)

Applying postConfigureOptions is crucial because OpenIdConnectHandler will throw exception that OpenIdConnectOptions is not correct configured.