Azure / data-api-builder

Data API builder provides modern REST and GraphQL endpoints to your Azure Databases and on-prem stores.
https://aka.ms/dab/docs
MIT License
949 stars 197 forks source link

AuthN Config V2 -> HotReload Aware Authentication settings in dev mode. #2414

Closed seantleonard closed 1 month ago

seantleonard commented 1 month ago

Why make this change?

What is this change?

The following changes are only honored in development mode when testing locally using "Hot Reload"

How was this tested?

Scenario 1 - Swap JWT Provider Details

  1. Open the dab-config.json file used by DAB.
  2. Ensure the config sets Mode: Development
  3. Modify the Authentication section to include JWT authN with your AzureAD provider (issuer/audience).
  4. Send a request which requires validation: see HTTP 200
  5. Modify the Authentication section with an "invalid" audience
  6. Select "SAVE" -> hot reload will occur
  7. Send a new request using the previously used token -> HTTP 401 invalid issuer.

Scenario 2 - Swap from JWT to Simulator

  1. Open the dab-config.json file used by DAB.
  2. Ensure the config sets Mode: Development
  3. Modify the Authentication section to include JWT authN with your AzureAD provider (issuer/audience).
  4. Send a request which requires validation: see HTTP 200
  5. Modify the Authentication section by removing the jwt section with audience/issuer and leave only the "Provider" property
  6. Modify the authentication provider property to be Simulator
  7. Select "SAVE" -> hot reload will occur
  8. Send a new request using the previously used token -> authenticated

Usage of ChangeTokens versus EventHandler in this specific workstream:

The objective was to trigger the IOptionsMonitor within the JwtBearerHandler to detect changes to authentication programmatically (as opposed to the out-of-box change detection for a bound configuration file such as appsettings.json, which we don't use). In order to alert the IOptionsMonitor that a change has occurred, I needed to register:

services.AddSingleton<IOptionsChangeTokenSource<JwtBearerOptions>>(new JwtBearerOptionsChangeTokenSource(runtimeConfigProvider));

which is this resolved via DI in the OptionsMonitor class: https://github.com/dotnet/runtime/blob/88397049d6aa2c8505bd11309ddd314169e3f73f/src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs#L31-L53

        public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
        {
            _factory = factory;
            _cache = cache;

            void RegisterSource(IOptionsChangeTokenSource<TOptions> source)
            {
                IDisposable registration = ChangeToken.OnChange(
                          source.GetChangeToken,
                          InvokeChanged,
                          source.Name);

                _registrations.Add(registration);
            }

            // The default DI container uses arrays under the covers. Take advantage of this knowledge
            // by checking for an array and enumerate over that, so we don't need to allocate an enumerator.
            if (sources is IOptionsChangeTokenSource<TOptions>[] sourcesArray)
            {
                foreach (IOptionsChangeTokenSource<TOptions> source in sourcesArray)
                {
                    RegisterSource(source);
                }

once our runtimeconfigloader detects a file change, it signals the provider that new runtimeconfig is available. Then the RuntimeConfigProvider (which is the service resolved via DI in most of our classes already) can signal the change token whose listener is the OptionsMonitor via registering the IOptionsChangeTokenSource.

This mechanism was not as intrusive to our services' constructors:

  1. No need to resolve IOptionsMonitorCache<jwtBearerOptions> in our RuntimeConfigProvider or RuntimeConfigLoader class constructors. Updating the optionsmonitorcache would be( i tested this) the manual way of changing IOptionsMonitor<JwtBearerOptions> used by the JwtBearerHandler, but this didn't work because of the next bullet point:
    1. I made a judgement call that injecting IoptionsmonitorCache<jwtBearerOptions> into either of the two referenced classes seemed to leak implementation details of the provider/loader and wouldn't be straightforward without more time consuming and costly refactors:
      • Because both the provider/loader are manually instantiated in Startup::ConfigureServices and don't have the opportunity to resolve services via DI.
      • Because we'd then need to make even more cascading changes in the test projects to accommodate setting up the ioptionsmonitorcache object to then be added to our mock runtimeconfigprovider/loader objects.

The event-handlers we have recently added don't fit this specific use case of updating the underlying authentication configuration. In addition to the above design decisions, I didn't identify a solution where we could subscribe the JwtBearerHandler's OptionsMonitor to an eventhandler signal to notify that a change occurred. I pursued using the built-in mechanism of change detection for optionsmonitor provided to us via IOptionsChangeTokenSource.

seantleonard commented 1 month ago

/azp run

seantleonard commented 1 month ago

/azp run

seantleonard commented 1 month ago

/azp run