From 1923b5831764278ff2cc741a5fe6dc57ae08f8df Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Wed, 9 Oct 2024 18:20:48 -0700 Subject: [PATCH 1/8] adding dynamic issuer/authorirty refresh for hotreload JwtBearer. --- src/Auth/Azure.DataApiBuilder.Auth.csproj | 2 +- src/Cli.Tests/Cli.Tests.csproj | 2 +- src/Cli/Cli.csproj | 2 +- src/Config/Azure.DataApiBuilder.Config.csproj | 4 +- src/Config/DabChangeToken.cs | 26 +++++ src/Config/FileSystemRuntimeConfigLoader.cs | 8 +- src/Config/RuntimeConfigLoader.cs | 45 ++++++++- .../SimulatorAuthenticationHandler.cs | 2 +- ...lientRoleHeaderAuthenticationMiddleware.cs | 54 ++++++++--- .../ConfigureJwtBearerOptions.cs | 25 +++++ ...EasyAuthAuthenticationBuilderExtensions.cs | 31 ++++++ .../EasyAuthAuthenticationDefaults.cs | 7 ++ .../EasyAuthAuthenticationHandler.cs | 2 +- .../GenericOAuthDefaults.cs | 9 ++ .../Authorization/AuthorizationResolver.cs | 2 +- src/Core/Azure.DataApiBuilder.Core.csproj | 3 +- src/Core/CollectionUtilities.cs | 21 ++++ .../Configurations/JwtConfigChangeRelay.cs | 97 +++++++++++++++++++ .../Configurations/RuntimeConfigProvider.cs | 32 ++++++ src/Core/Resolvers/SqlMutationEngine.cs | 1 - src/Directory.Packages.props | 27 +++--- .../Azure.DataApiBuilder.Product.csproj | 2 +- ...taApiBuilder.Service.GraphQLBuilder.csproj | 2 +- .../Azure.DataApiBuilder.Service.Tests.csproj | 2 +- .../Azure.DataApiBuilder.Service.csproj | 2 +- .../Controllers/ConfigurationController.cs | 31 +++++- .../JwtBearerOptionsChangeTokenSource.cs | 29 ++++++ src/Service/Startup.cs | 25 ++++- 28 files changed, 441 insertions(+), 54 deletions(-) create mode 100644 src/Config/DabChangeToken.cs create mode 100644 src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs create mode 100644 src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs create mode 100644 src/Core/CollectionUtilities.cs create mode 100644 src/Core/Configurations/JwtConfigChangeRelay.cs create mode 100644 src/Service/JwtBearerOptionsChangeTokenSource.cs diff --git a/src/Auth/Azure.DataApiBuilder.Auth.csproj b/src/Auth/Azure.DataApiBuilder.Auth.csproj index 9f63cd3ed6..1ee2df57b8 100644 --- a/src/Auth/Azure.DataApiBuilder.Auth.csproj +++ b/src/Auth/Azure.DataApiBuilder.Auth.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Cli.Tests/Cli.Tests.csproj b/src/Cli.Tests/Cli.Tests.csproj index 46192e3802..a8081f5250 100644 --- a/src/Cli.Tests/Cli.Tests.csproj +++ b/src/Cli.Tests/Cli.Tests.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0 enable enable false diff --git a/src/Cli/Cli.csproj b/src/Cli/Cli.csproj index fec0cc9786..5321f52356 100644 --- a/src/Cli/Cli.csproj +++ b/src/Cli/Cli.csproj @@ -2,7 +2,7 @@ Exe - net8.0;net6.0 + net8.0 Cli enable enable diff --git a/src/Config/Azure.DataApiBuilder.Config.csproj b/src/Config/Azure.DataApiBuilder.Config.csproj index 501f10bc22..735c88cc16 100644 --- a/src/Config/Azure.DataApiBuilder.Config.csproj +++ b/src/Config/Azure.DataApiBuilder.Config.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0 enable enable $(BaseOutputPath)\engine @@ -16,6 +16,8 @@ + + diff --git a/src/Config/DabChangeToken.cs b/src/Config/DabChangeToken.cs new file mode 100644 index 0000000000..2311e34130 --- /dev/null +++ b/src/Config/DabChangeToken.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Primitives; + +namespace Azure.DataApiBuilder.Config; + +public class DabChangeToken : IChangeToken +{ + private CancellationTokenSource _cts = new(); + + public bool HasChanged => _cts.IsCancellationRequested; + + public bool ActiveChangeCallbacks => true; + + public IDisposable RegisterChangeCallback(Action callback, object? state) + { + return _cts.Token.Register(callback, state); + } + + public void SignalChange() + { + _cts.Cancel(); + } +} + diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index ace3aa8e7d..447ee4be57 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -60,7 +60,11 @@ public class FileSystemRuntimeConfigLoader : RuntimeConfigLoader /// public string ConfigFilePath { get; internal set; } - public FileSystemRuntimeConfigLoader(IFileSystem fileSystem, HotReloadEventHandler? handler = null, string baseConfigFilePath = DEFAULT_CONFIG_FILE_NAME, string? connectionString = null) + public FileSystemRuntimeConfigLoader( + IFileSystem fileSystem, + HotReloadEventHandler? handler = null, + string baseConfigFilePath = DEFAULT_CONFIG_FILE_NAME, + string? connectionString = null) : base(handler, connectionString) { _fileSystem = fileSystem; @@ -154,7 +158,7 @@ public bool TryLoadConfig( if (!string.IsNullOrEmpty(defaultDataSourceName)) { - RuntimeConfig.DefaultDataSourceName = defaultDataSourceName; + RuntimeConfig.UpdateDefaultDataSourceName(defaultDataSourceName); } config = RuntimeConfig; diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index acdb3f4aa3..1cffa0cae4 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -14,7 +14,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; +using Microsoft.Extensions.Primitives; using Npgsql; [assembly: InternalsVisibleTo("Azure.DataApiBuilder.Service.Tests")] @@ -22,6 +22,11 @@ namespace Azure.DataApiBuilder.Config; public abstract class RuntimeConfigLoader { + // Change Token Code + private DabChangeToken _changeToken; + private readonly List _changeTokenRegistrations; + + // Other private HotReloadEventHandler? _handler; protected readonly string? _connectionString; @@ -30,6 +35,18 @@ public abstract class RuntimeConfigLoader // state in place of using out params. public RuntimeConfig? RuntimeConfig; + public RuntimeConfigLoader(HotReloadEventHandler? handler = null, string? connectionString = null) + { + _changeToken = new DabChangeToken(); + _changeTokenRegistrations = new List(1) + { + ChangeToken.OnChange(GetChangeToken, PostChangeTokenChangedAction) + }; + + _handler = handler; + _connectionString = connectionString; + } + // Signals a hot reload event for OpenApiDocumentor due to config change. protected virtual void DocumentorOnConfigChanged(HotReloadEventArgs args) { @@ -41,12 +58,30 @@ public void SendEventNotification(string message = "") { HotReloadEventArgs args = new(message); DocumentorOnConfigChanged(args); + RaiseChanged(); } - public RuntimeConfigLoader(HotReloadEventHandler? handler = null, string? connectionString = null) + // Raises the changed event. + private void RaiseChanged() { - _handler = handler; - _connectionString = connectionString; + DabChangeToken previousToken = Interlocked.Exchange(ref _changeToken, new DabChangeToken()); + previousToken.SignalChange(); + } + + private void PostChangeTokenChangedAction() + { + Console.WriteLine("Howdy! Change token has been signalled :)"); + } + + /// + /// producer + /// + /// +#pragma warning disable CA1024 // Use properties where appropriate + public IChangeToken GetChangeToken() +#pragma warning restore CA1024 // Use properties where appropriate + { + return _changeToken; } /// @@ -299,7 +334,7 @@ internal static string GetPgSqlConnectionStringWithApplicationName(string connec // If the connection string does not contain the `Application Name` property, add it. // or if the connection string contains the `Application Name` property, replace it with the DataApiBuilder Application Name. - if (connectionStringBuilder.ApplicationName.IsNullOrEmpty()) + if (string.IsNullOrEmpty(connectionStringBuilder.ApplicationName)) { connectionStringBuilder.ApplicationName = applicationName; } diff --git a/src/Core/AuthenticationHelpers/AuthenticationSimulator/SimulatorAuthenticationHandler.cs b/src/Core/AuthenticationHelpers/AuthenticationSimulator/SimulatorAuthenticationHandler.cs index df9e90e9b9..4e5970778b 100644 --- a/src/Core/AuthenticationHelpers/AuthenticationSimulator/SimulatorAuthenticationHandler.cs +++ b/src/Core/AuthenticationHelpers/AuthenticationSimulator/SimulatorAuthenticationHandler.cs @@ -85,7 +85,7 @@ protected override Task HandleAuthenticateAsync() // AuthenticationTicket is Asp.Net Core Abstraction of Authentication information // of the authenticated user. - AuthenticationTicket ticket = new(claimsPrincipal, EasyAuthAuthenticationDefaults.AUTHENTICATIONSCHEME); + AuthenticationTicket ticket = new(claimsPrincipal, SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME); AuthenticateResult success = AuthenticateResult.Success(ticket); return Task.FromResult(success); } diff --git a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs index f364d4b2e8..3e70e1b44a 100644 --- a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs +++ b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs @@ -3,10 +3,12 @@ using System.Security.Claims; using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator; using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Core.Models; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -23,11 +25,11 @@ namespace Azure.DataApiBuilder.Core.AuthenticationHelpers; /// public class ClientRoleHeaderAuthenticationMiddleware { + private const string ANONYOUMOUS_ROLE = "Anonymous"; + private const string AUTHENTICATED_ROLE = "Authenticated"; private readonly RequestDelegate _nextMiddleware; - private ILogger _logger; - - private bool _isLateConfigured; + private RuntimeConfigProvider _runtimeConfigProvider; // Identity provider used for identities added to the ClaimsPrincipal object for the current user by DAB. private const string INTERNAL_DAB_IDENTITY_PROVIDER = "DAB-VERIFIED"; @@ -38,7 +40,7 @@ public ClientRoleHeaderAuthenticationMiddleware(RequestDelegate next, { _nextMiddleware = next; _logger = logger; - _isLateConfigured = runtimeConfigProvider.IsLateConfigured; + _runtimeConfigProvider = runtimeConfigProvider; } /// @@ -61,7 +63,9 @@ public async Task InvokeAsync(HttpContext httpContext) // 1. Succeeded - Authenticated // 2. Failure - Token issue // 3. None - No token provided, no auth result. - AuthenticateResult authNResult = await httpContext.AuthenticateAsync(); + AuthenticationOptions? dabAuthNOptions = _runtimeConfigProvider.GetConfig().Runtime?.Host?.Authentication; + string scheme = ResolveConfiguredAuthNScheme(dabAuthNOptions?.Provider); + AuthenticateResult authNResult = await httpContext.AuthenticateAsync(scheme); // Reject and terminate the request when an invalid token is provided // Write challenge response metadata (HTTP 401 Unauthorized response code @@ -69,11 +73,19 @@ public async Task InvokeAsync(HttpContext httpContext) // https://github.com/dotnet/aspnetcore/blob/3fe12b935c03138f76364dc877a7e069e254b5b2/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs#L217 if (authNResult.Failure is not null) { - await httpContext.ChallengeAsync(); + await httpContext.ChallengeAsync(scheme); return; } - string clientDefinedRole = AuthorizationType.Anonymous.ToString(); + // Manually set the httpContext.User to the Principal from the AuthenticateResult + // when we exclude setting a default authentication scheme in Startup.cs AddAuthentication(). + // https://learn.microsoft.com/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-8.0 + if (authNResult.Succeeded) + { + httpContext.User = authNResult.Principal; + } + + string clientDefinedRole = ANONYOUMOUS_ROLE; // A request can be authenticated in 2 cases: // 1. When the request has a valid jwt/easyauth token, @@ -82,7 +94,7 @@ public async Task InvokeAsync(HttpContext httpContext) if (isAuthenticatedRequest) { - clientDefinedRole = AuthorizationType.Authenticated.ToString(); + clientDefinedRole = AUTHENTICATED_ROLE; } // Attempt to inject CLIENT_ROLE_HEADER:clientDefinedRole into the httpContext @@ -106,17 +118,16 @@ public async Task InvokeAsync(HttpContext httpContext) // Log the request's authenticated status (anonymous/authenticated) and user role, // only in the non-hosted scenario. - if (!_isLateConfigured) + if (!_runtimeConfigProvider.IsLateConfigured) { string correlationId = HttpContextExtensions.GetLoggerCorrelationId(httpContext); string requestAuthStatus = isAuthenticatedRequest ? AuthorizationType.Authenticated.ToString() : AuthorizationType.Anonymous.ToString(); _logger.LogDebug( - message: "{correlationId} Request authentication state: {requestAuthStatus}.", - correlationId, - requestAuthStatus); - _logger.LogDebug("{correlationId} The request will be executed in the context of the role: {clientDefinedRole}", + message: "{correlationId} AuthN state: {requestAuthStatus}. Role: {clientDefinedRole}. Scheme: {scheme}", correlationId, - clientDefinedRole); + requestAuthStatus, + clientDefinedRole, + scheme); } // When the user is not in the clientDefinedRole and the client role header @@ -151,6 +162,21 @@ public static bool IsSystemRole(string roleName) return roleName.Equals(AuthorizationType.Authenticated.ToString(), StringComparison.OrdinalIgnoreCase) || roleName.Equals(AuthorizationType.Anonymous.ToString(), StringComparison.OrdinalIgnoreCase); } + + private static string ResolveConfiguredAuthNScheme(string? configuredProviderName) + { + switch (configuredProviderName) + { + case AuthenticationOptions.SIMULATOR_AUTHENTICATION: + return SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME; + case nameof(EasyAuthType.AppService): + return EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME; + case "AzureAD": + return JwtBearerDefaults.AuthenticationScheme; + default: + return EasyAuthAuthenticationDefaults.SWAAUTHSCHEME; + } + } } // Extension method used to add the middleware to the HTTP request pipeline. diff --git a/src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs b/src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs new file mode 100644 index 0000000000..7d91ce2fd3 --- /dev/null +++ b/src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; + +namespace Azure.DataApiBuilder.Service; + +public class ConfigureJwtBearerOptions : IConfigureNamedOptions +{ + public ConfigureJwtBearerOptions() + { + } + + // Configure the named instance + public void Configure(string? name, JwtBearerOptions options) + { + // Only configure the options if this is the correct instance + Console.WriteLine("HEY THERE ! Configuring JwtBearerOptions for: " + name); + options.Audience = "New AUdience?"; + } + + // This won't be called, but is required for the interface + public void Configure(JwtBearerOptions options) => Configure(Options.DefaultName, options); +} diff --git a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs index 2c15e589df..5c835f33a4 100644 --- a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs +++ b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs @@ -43,4 +43,35 @@ public static AuthenticationBuilder AddEasyAuthAuthentication( }); return builder; } + + public static AuthenticationBuilder AddEnvDetectedEasyAuth(this AuthenticationBuilder builder) + { + if (builder is null) + { + throw new System.ArgumentNullException(nameof(builder)); + } + + builder.AddScheme( + authenticationScheme: EasyAuthAuthenticationDefaults.SWAAUTHSCHEME, + displayName: EasyAuthAuthenticationDefaults.SWAAUTHSCHEME, + options => + { + options.EasyAuthProvider = EasyAuthType.StaticWebApps; + }); + + bool appServiceEnvironmentDetected = AppServiceAuthenticationInfo.AreExpectedAppServiceEnvVarsPresent(); + + if (appServiceEnvironmentDetected) + { + builder.AddScheme( + authenticationScheme: EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME, + displayName: EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME, + options => + { + options.EasyAuthProvider = EasyAuthType.AppService; + }); + } + + return builder; + } } diff --git a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs index 32e6893b71..f44bd47d36 100644 --- a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs +++ b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs @@ -13,5 +13,12 @@ public static class EasyAuthAuthenticationDefaults /// public const string AUTHENTICATIONSCHEME = "EasyAuthAuthentication"; + /// + /// EasyAuth authentication scheme names granularized by provider + /// to enable compatility with HotReloading authentication settings. + /// + public const string SWAAUTHSCHEME = "StaticWebAppsAuthentication"; + public const string APPSERVICEAUTHSCHEME = "AppServiceAuthentication"; + public const string INVALID_PAYLOAD_ERROR = "Invalid EasyAuth Payload."; } diff --git a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs index d74139fa39..4ce45a0543 100644 --- a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs +++ b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs @@ -67,7 +67,7 @@ protected override Task HandleAuthenticateAsync() { if (Context.Request.Headers[AuthenticationOptions.CLIENT_PRINCIPAL_HEADER].Count > 0) { - ClaimsIdentity? identity = Options.EasyAuthProvider switch + ClaimsIdentity? identity = OptionsMonitor.CurrentValue.EasyAuthProvider switch { EasyAuthType.StaticWebApps => StaticWebAppsAuthentication.Parse(Context, Logger), EasyAuthType.AppService => AppServiceAuthentication.Parse(Context, Logger), diff --git a/src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs b/src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs new file mode 100644 index 0000000000..29569f7b91 --- /dev/null +++ b/src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.DataApiBuilder.Core.AuthenticationHelpers; + +public class GenericOAuthDefaults +{ + public const string AUTHENTICATIONSCHEME = "OAuthAuthentication"; +} diff --git a/src/Core/Authorization/AuthorizationResolver.cs b/src/Core/Authorization/AuthorizationResolver.cs index 64785de703..90bf03f82a 100644 --- a/src/Core/Authorization/AuthorizationResolver.cs +++ b/src/Core/Authorization/AuthorizationResolver.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Security.Claims; using System.Text.Json; @@ -16,6 +15,7 @@ using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.JsonWebTokens; namespace Azure.DataApiBuilder.Core.Authorization; diff --git a/src/Core/Azure.DataApiBuilder.Core.csproj b/src/Core/Azure.DataApiBuilder.Core.csproj index d8e314d78d..9c1ce9aeb8 100644 --- a/src/Core/Azure.DataApiBuilder.Core.csproj +++ b/src/Core/Azure.DataApiBuilder.Core.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0 enable enable true @@ -16,6 +16,7 @@ + diff --git a/src/Core/CollectionUtilities.cs b/src/Core/CollectionUtilities.cs new file mode 100644 index 0000000000..48968fd726 --- /dev/null +++ b/src/Core/CollectionUtilities.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.DataApiBuilder.Core; + +/// +/// A class which contains useful methods for processing collections. +/// +internal static class CollectionUtilities +{ + /// + /// Checks whether is null or empty. + /// + /// The type of the . + /// The to be checked. + /// True if is null or empty, false otherwise. + public static bool IsNullOrEmpty(this IEnumerable enumerable) + { + return enumerable == null || !enumerable.Any(); + } +} diff --git a/src/Core/Configurations/JwtConfigChangeRelay.cs b/src/Core/Configurations/JwtConfigChangeRelay.cs new file mode 100644 index 0000000000..8b34c7a3cf --- /dev/null +++ b/src/Core/Configurations/JwtConfigChangeRelay.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.DataApiBuilder.Config.ObjectModel; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Validators; +using static NodaTime.TimeZones.ZoneEqualityComparer; + +namespace Azure.DataApiBuilder.Core.Configurations; + +public class JwtConfigChangeRelay +{ + private readonly List _registrations = new(); + private readonly IOptionsMonitor _bearerOptionsMonitor; + private readonly IOptionsMonitorCache _bearerOptionsMonitorCache; + private readonly IPostConfigureOptions[] _postConfigures; + private readonly RuntimeConfigProvider _runtimeConfigProvider; + private readonly List _changeTokenRegistrations; + + public JwtConfigChangeRelay( + IEnumerable> sources, + IOptionsMonitor bearerOptionsMonitor, + IOptionsMonitorCache jwtOptionsMonitorCache, + IEnumerable> jwtPostConfigureOptions, + RuntimeConfigProvider runtimeConfigProvider) + { + _bearerOptionsMonitor = bearerOptionsMonitor; + _bearerOptionsMonitorCache = jwtOptionsMonitorCache; + _postConfigures = jwtPostConfigureOptions as IPostConfigureOptions[] ?? new List>(jwtPostConfigureOptions).ToArray(); + _runtimeConfigProvider = runtimeConfigProvider; + + _changeTokenRegistrations = new List(1) + { + ChangeToken.OnChange(_runtimeConfigProvider.GetChangeToken,PostChangeTokenChangedAction, false) + }; + } + + //https://github.com/dotnet/aspnetcore/issues/52296 + public void ConfigureJwtBearerProvider() + { + AuthenticationOptions? newAuthOptions = _runtimeConfigProvider.GetConfig().Runtime?.Host?.Authentication; + + JwtBearerOptions jwtOptions = new() + { + MapInboundClaims = false, + Audience = newAuthOptions?.Jwt?.Audience, + Authority = newAuthOptions?.Jwt?.Issuer, + TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters() + { + ValidAudience = newAuthOptions?.Jwt?.Audience, + ValidIssuers = new List() { "https://login.microsoftonline.com/291bf275-ea78-4cde-84ea-21309a43a567/v2.0" }, + // Instructs the asp.net core middleware to use the data in the "roles" claim for User.IsInRole() + // See https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsprincipal.isinrole?view=net-6.0#remarks + RoleClaimType = AuthenticationOptions.ROLE_CLAIM_TYPE + } + }; + + //jwtOptions.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); + //jwtOptions.TokenValidationParameters.SignatureValidator = (token, _) => new JsonWebToken(token); + _bearerOptionsMonitorCache.TryRemove(JwtBearerDefaults.AuthenticationScheme); + foreach (IPostConfigureOptions post in _postConfigures) + { + post.PostConfigure(JwtBearerDefaults.AuthenticationScheme, jwtOptions); + } + // _jwtPostConfigureOptions.PostConfigure(JwtBearerDefaults.AuthenticationScheme, jwtOptions); + _bearerOptionsMonitorCache.GetOrAdd(JwtBearerDefaults.AuthenticationScheme, () => jwtOptions); + Console.WriteLine("New Audience: " + _bearerOptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme).Audience); + Console.WriteLine("New Issuer: " + _bearerOptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme).Authority); + + } + + private void PostChangeTokenChangedAction(bool forceUpdate = false) + { + // Skip hot reload circumstances in ProdMode. + if (!forceUpdate && (_runtimeConfigProvider.IsLateConfigured || _runtimeConfigProvider.GetConfig().Runtime?.Host?.Mode == HostMode.Production)) + { + return; + } + //Console.WriteLine("Old Audience: " + jwtOptions.Audience); + JwtBearerOptions jwtOptions = _bearerOptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme); + //Console.WriteLine("Current options: " + jwtOptions.Audience); + AuthenticationOptions? newAuthOptions = _runtimeConfigProvider.GetConfig().Runtime?.Host?.Authentication; + + if (_bearerOptionsMonitorCache.TryRemove(JwtBearerDefaults.AuthenticationScheme)) + { + jwtOptions.Audience = newAuthOptions?.Jwt?.Audience; + jwtOptions.TokenValidationParameters.ValidAudience = newAuthOptions?.Jwt?.Audience; + jwtOptions.Authority = newAuthOptions?.Jwt?.Issuer; + // _jwtPostConfigureOptions.PostConfigure(JwtBearerDefaults.AuthenticationScheme, jwtOptions); + _bearerOptionsMonitorCache.GetOrAdd(JwtBearerDefaults.AuthenticationScheme, () => jwtOptions); + Console.WriteLine("New Audience: " + _bearerOptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme).Audience); + } + } +} diff --git a/src/Core/Configurations/RuntimeConfigProvider.cs b/src/Core/Configurations/RuntimeConfigProvider.cs index 79f63d5b7f..491f3db3cc 100644 --- a/src/Core/Configurations/RuntimeConfigProvider.cs +++ b/src/Core/Configurations/RuntimeConfigProvider.cs @@ -9,6 +9,7 @@ using Azure.DataApiBuilder.Config.NamingPolicies; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; +using Microsoft.Extensions.Primitives; namespace Azure.DataApiBuilder.Core.Configurations; @@ -42,6 +43,27 @@ public class RuntimeConfigProvider public Dictionary ManagedIdentityAccessToken { get; private set; } = new Dictionary(); private RuntimeConfigLoader _configLoader; + private DabChangeToken _changeToken = new(); + private readonly List? _changeTokenRegistrations; + private JwtConfigChangeRelay? _changeRelay; + + // Raises the changed event. + private void RaiseChanged() + { + DabChangeToken previousToken = Interlocked.Exchange(ref _changeToken, new DabChangeToken()); + previousToken.SignalChange(); + } + + /// + /// producer + /// + /// +#pragma warning disable CA1024 // Use properties where appropriate + public IChangeToken GetChangeToken() +#pragma warning restore CA1024 // Use properties where appropriate + { + return _changeToken; + } /// /// Accessor for the ConfigFilePath to avoid exposing the loader. If we are not @@ -60,6 +82,16 @@ public string ConfigFilePath } } + public RuntimeConfigProvider(RuntimeConfigLoader runtimeConfigLoader, JwtConfigChangeRelay jwtConfigChangeRelay) + { + _configLoader = runtimeConfigLoader; + _changeRelay = jwtConfigChangeRelay; + _changeTokenRegistrations = new List(1) + { + ChangeToken.OnChange(_configLoader.GetChangeToken, RaiseChanged) + }; + } + public RuntimeConfigProvider(RuntimeConfigLoader runtimeConfigLoader) { _configLoader = runtimeConfigLoader; diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index e70560367b..388b980ebf 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -27,7 +27,6 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; -using Microsoft.IdentityModel.Tokens; namespace Azure.DataApiBuilder.Core.Resolvers { diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 69f539dee4..35041fa787 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -22,7 +22,11 @@ - + + + + + @@ -39,7 +43,7 @@ - + @@ -49,21 +53,12 @@ - - - - + + + + - - - - - - - - - - + \ No newline at end of file diff --git a/src/Product/Azure.DataApiBuilder.Product.csproj b/src/Product/Azure.DataApiBuilder.Product.csproj index f0d0e70927..6045200487 100644 --- a/src/Product/Azure.DataApiBuilder.Product.csproj +++ b/src/Product/Azure.DataApiBuilder.Product.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj b/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj index 649e46c550..87eab3c75b 100644 --- a/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj +++ b/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj b/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj index 14722aea7d..8badb7d90d 100644 --- a/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj +++ b/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0 false disable $(BaseOutputPath)\tests diff --git a/src/Service/Azure.DataApiBuilder.Service.csproj b/src/Service/Azure.DataApiBuilder.Service.csproj index 710048b9fa..5cb3be73b2 100644 --- a/src/Service/Azure.DataApiBuilder.Service.csproj +++ b/src/Service/Azure.DataApiBuilder.Service.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0 Debug;Release;Docker $(BaseOutputPath)\engine win-x64;linux-x64;osx-x64 diff --git a/src/Service/Controllers/ConfigurationController.cs b/src/Service/Controllers/ConfigurationController.cs index be3f9bd727..085fdcf7a3 100644 --- a/src/Service/Controllers/ConfigurationController.cs +++ b/src/Service/Controllers/ConfigurationController.cs @@ -6,7 +6,9 @@ using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Core.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Azure.DataApiBuilder.Service.Controllers @@ -17,9 +19,11 @@ public class ConfigurationController : Controller { RuntimeConfigProvider _configurationProvider; private readonly ILogger _logger; + private readonly IConfiguration _configuration; - public ConfigurationController(RuntimeConfigProvider configurationProvider, ILogger logger) + public ConfigurationController(RuntimeConfigProvider configurationProvider, ILogger logger, IConfiguration configuration) { + _configuration = configuration; _configurationProvider = configurationProvider; _logger = logger; } @@ -70,6 +74,31 @@ public async Task Index([FromBody] ConfigurationPostParametersV2 c return BadRequest(); } + [HttpPost("changeJwtProvider")] + public ActionResult ChangeJwtProvider([FromBody] JwtConfigPostParameters jwtConfig) + { + try + { + _configuration.GetSection("JwtBearer"); + _configuration["Authentication:Schemes:Bearer:ValidAudiences"] = jwtConfig.Audience; + } + catch (Exception e) + { + _logger.LogError( + exception: e, + message: "{correlationId} Exception during configuration initialization.", + HttpContextExtensions.GetLoggerCorrelationId(HttpContext)); + } + + return BadRequest(); + } + + public record class JwtConfigPostParameters( + string SchemeName, + string Audience, + string Authority) + { } + /// /// Takes in the runtime configuration, schema, connection string and access token and configures the runtime. /// If the runtime is already configured, it will return a conflict result. diff --git a/src/Service/JwtBearerOptionsChangeTokenSource.cs b/src/Service/JwtBearerOptionsChangeTokenSource.cs new file mode 100644 index 0000000000..bd71853a12 --- /dev/null +++ b/src/Service/JwtBearerOptionsChangeTokenSource.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.DataApiBuilder.Config; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Azure.DataApiBuilder.Service; + +public class JwtBearerOptionsChangeTokenSource : IOptionsChangeTokenSource +{ + //private readonly DabChangeToken _changeToken; + private readonly RuntimeConfigLoader _configLoader; + + public JwtBearerOptionsChangeTokenSource(RuntimeConfigLoader configLoader) + { + // Change event source is the provider. + _configLoader = configLoader; + } + + public string Name => "DogToken"; + + public IChangeToken GetChangeToken() + { + return _configLoader.GetChangeToken(); + } +} + diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index e0653d90a5..5d098bb97c 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -41,6 +41,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using ZiggyCreatures.Caching.Fusion; using CorsOptions = Azure.DataApiBuilder.Config.ObjectModel.CorsOptions; @@ -87,10 +88,10 @@ public void ConfigureServices(IServiceCollection services) FileSystemRuntimeConfigLoader configLoader = new(fileSystem, hotReloadEventHandler, configFileName, connectionString); RuntimeConfigProvider configProvider = new(configLoader); + services.AddSingleton(); services.AddSingleton(fileSystem); - services.AddSingleton(configProvider); services.AddSingleton(configLoader); - + services.AddSingleton(configProvider); if (configProvider.TryGetConfig(out RuntimeConfig? runtimeConfig) && runtimeConfig.Runtime?.Telemetry?.ApplicationInsights is not null && runtimeConfig.Runtime.Telemetry.ApplicationInsights.Enabled) @@ -174,7 +175,7 @@ public void ConfigureServices(IServiceCollection services) //Enable accessing HttpContext in RestService to get ClaimsPrincipal. services.AddHttpContextAccessor(); - ConfigureAuthentication(services, configProvider); + ConfigureAuthV2(services, configLoader); services.AddAuthorization(); services.AddSingleton>(implementationFactory: (serviceProvider) => @@ -524,6 +525,22 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP } } + /// + /// Wires up all DAB supported authentication providers. DAB uses the user configured + /// authentcation provider to determine which provider to use for authentication at request time. + /// + private void ConfigureAuthV2(IServiceCollection services, RuntimeConfigLoader configLoader) + { + services.AddSingleton>(new JwtBearerOptionsChangeTokenSource(configLoader)); + services.AddSingleton>(new ConfigurationChangeTokenSource(JwtBearerDefaults.AuthenticationScheme, Configuration)); + //services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerPostConfigureOptions>()); + // Purposely + services.AddAuthentication() + .AddEnvDetectedEasyAuth() + .AddJwtBearer() + .AddSimulatorAuthentication(); + } + /// /// Configure Application Insights Telemetry based on the loaded runtime configuration. If Application Insights /// is enabled, we can track different events and metrics. @@ -606,6 +623,8 @@ private async Task PerformOnConfigChangeAsync(IApplicationBuilder app) { // Running only in developer mode to ensure fast and smooth startup in production. runtimeConfigValidator.ValidatePermissionsInConfig(runtimeConfig); + JwtConfigChangeRelay relay = app.ApplicationServices.GetService()!; + relay.ConfigureJwtBearerProvider(); } IMetadataProviderFactory sqlMetadataProviderFactory = From ddb315f79a0303716ab470d21ef373f4d3c0f024 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Thu, 10 Oct 2024 13:46:29 -0700 Subject: [PATCH 2/8] Consolidated code. Now need to add comments. --- src/Cli.Tests/TestHelper.cs | 2 - src/Cli/CollectionUtilities.cs | 21 ++++ src/Cli/ConfigGenerator.cs | 1 - src/Config/RuntimeConfigLoader.cs | 43 +++----- ...lientRoleHeaderAuthenticationMiddleware.cs | 6 +- .../ConfigureJwtBearerOptions.cs | 40 +++++++- src/Core/CollectionUtilities.cs | 2 +- .../Configurations/JwtConfigChangeRelay.cs | 97 ------------------- .../Configurations/RuntimeConfigProvider.cs | 36 +++---- .../Configuration/ConfigurationTests.cs | 1 + .../Configuration/TelemetryTests.cs | 2 +- .../JwtBearerOptionsChangeTokenSource.cs | 12 +-- src/Service/Startup.cs | 31 ++++-- 13 files changed, 125 insertions(+), 169 deletions(-) create mode 100644 src/Cli/CollectionUtilities.cs delete mode 100644 src/Core/Configurations/JwtConfigChangeRelay.cs diff --git a/src/Cli.Tests/TestHelper.cs b/src/Cli.Tests/TestHelper.cs index 0d2ff52ffe..6e1e7aefe5 100644 --- a/src/Cli.Tests/TestHelper.cs +++ b/src/Cli.Tests/TestHelper.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.IdentityModel.Tokens; - namespace Cli.Tests { public static class TestHelper diff --git a/src/Cli/CollectionUtilities.cs b/src/Cli/CollectionUtilities.cs new file mode 100644 index 0000000000..6fd0b606dc --- /dev/null +++ b/src/Cli/CollectionUtilities.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Cli; + +/// +/// A class which contains useful methods for processing collections. +/// +internal static class CollectionUtilities +{ + /// + /// Checks whether is null or empty. + /// + /// The type of the . + /// The to be checked. + /// True if is null or empty, false otherwise. + public static bool IsNullOrEmpty(this IEnumerable? enumerable) + { + return enumerable == null || !enumerable.Any(); + } +} diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index b5cda43218..c57b0f4f08 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -12,7 +12,6 @@ using Azure.DataApiBuilder.Service; using Cli.Commands; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; using static Cli.Utils; namespace Cli diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index 1cffa0cae4..c6deb140dd 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -22,11 +22,7 @@ namespace Azure.DataApiBuilder.Config; public abstract class RuntimeConfigLoader { - // Change Token Code private DabChangeToken _changeToken; - private readonly List _changeTokenRegistrations; - - // Other private HotReloadEventHandler? _handler; protected readonly string? _connectionString; @@ -38,27 +34,16 @@ public abstract class RuntimeConfigLoader public RuntimeConfigLoader(HotReloadEventHandler? handler = null, string? connectionString = null) { _changeToken = new DabChangeToken(); - _changeTokenRegistrations = new List(1) - { - ChangeToken.OnChange(GetChangeToken, PostChangeTokenChangedAction) - }; - _handler = handler; _connectionString = connectionString; } - // Signals a hot reload event for OpenApiDocumentor due to config change. - protected virtual void DocumentorOnConfigChanged(HotReloadEventArgs args) - { - _handler?.DocumentorOnConfigChangedEvent(this, args); - } - - // Sends all of the notifications when a hot reload occurs. - public void SendEventNotification(string message = "") + // Signals changes to the config to listeners. +#pragma warning disable CA1024 // Use properties where appropriate + public IChangeToken GetChangeToken() +#pragma warning restore CA1024 // Use properties where appropriate { - HotReloadEventArgs args = new(message); - DocumentorOnConfigChanged(args); - RaiseChanged(); + return _changeToken; } // Raises the changed event. @@ -68,20 +53,18 @@ private void RaiseChanged() previousToken.SignalChange(); } - private void PostChangeTokenChangedAction() + // Signals a hot reload event for OpenApiDocumentor due to config change. + protected virtual void DocumentorOnConfigChanged(HotReloadEventArgs args) { - Console.WriteLine("Howdy! Change token has been signalled :)"); + _handler?.DocumentorOnConfigChangedEvent(this, args); } - /// - /// producer - /// - /// -#pragma warning disable CA1024 // Use properties where appropriate - public IChangeToken GetChangeToken() -#pragma warning restore CA1024 // Use properties where appropriate + // Sends all of the notifications when a hot reload occurs. + public void SendEventNotification(string message = "") { - return _changeToken; + HotReloadEventArgs args = new(message); + DocumentorOnConfigChanged(args); + RaiseChanged(); } /// diff --git a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs index 3e70e1b44a..8887865501 100644 --- a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs +++ b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs @@ -171,10 +171,14 @@ private static string ResolveConfiguredAuthNScheme(string? configuredProviderNam return SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME; case nameof(EasyAuthType.AppService): return EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME; + case nameof(EasyAuthType.StaticWebApps): + return EasyAuthAuthenticationDefaults.SWAAUTHSCHEME; case "AzureAD": + case "EntraID": return JwtBearerDefaults.AuthenticationScheme; + case "Custom": default: - return EasyAuthAuthenticationDefaults.SWAAUTHSCHEME; + return GenericOAuthDefaults.AUTHENTICATIONSCHEME; } } } diff --git a/src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs b/src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs index 7d91ce2fd3..4f05e6f804 100644 --- a/src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs +++ b/src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs @@ -1,23 +1,57 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Configurations; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Validators; namespace Azure.DataApiBuilder.Service; public class ConfigureJwtBearerOptions : IConfigureNamedOptions { - public ConfigureJwtBearerOptions() + private readonly RuntimeConfigProvider _runtimeConfigProvider; + public ConfigureJwtBearerOptions(RuntimeConfigProvider runtimeConfigProvider) { + _runtimeConfigProvider = runtimeConfigProvider; } // Configure the named instance public void Configure(string? name, JwtBearerOptions options) { + // Skip JwtBearerOptions hot-reload circumstances in ProdMode. + if (!_runtimeConfigProvider.IsConfigHotReloadable()) + { + // perhaps have this check in the change token ? + return; + } + + AuthenticationOptions? newAuthOptions = _runtimeConfigProvider.GetConfig().Runtime?.Host?.Authentication; + + // Don't configure JwtBearerOptions when JWT properties(issuer/audience) are excluded. + if (newAuthOptions is null || newAuthOptions.Jwt is null) + { + return; + } + // Only configure the options if this is the correct instance - Console.WriteLine("HEY THERE ! Configuring JwtBearerOptions for: " + name); - options.Audience = "New AUdience?"; + options.MapInboundClaims = false; + options.Audience = newAuthOptions.Jwt.Audience; + options.Authority = newAuthOptions.Jwt.Issuer; + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters() + { + ValidAudience = newAuthOptions.Jwt.Audience, + ValidIssuer = newAuthOptions.Jwt.Issuer, + // Instructs the asp.net core middleware to use the data in the "roles" claim for User.IsInRole() + // See https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsprincipal.isinrole?view=net-6.0#remarks + RoleClaimType = AuthenticationOptions.ROLE_CLAIM_TYPE + }; + + if (newAuthOptions.Provider.Equals("AzureAD") || newAuthOptions.Provider.Equals("EntraID")) + { + options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); + } } // This won't be called, but is required for the interface diff --git a/src/Core/CollectionUtilities.cs b/src/Core/CollectionUtilities.cs index 48968fd726..12d123bb07 100644 --- a/src/Core/CollectionUtilities.cs +++ b/src/Core/CollectionUtilities.cs @@ -14,7 +14,7 @@ internal static class CollectionUtilities /// The type of the . /// The to be checked. /// True if is null or empty, false otherwise. - public static bool IsNullOrEmpty(this IEnumerable enumerable) + public static bool IsNullOrEmpty(this IEnumerable? enumerable) { return enumerable == null || !enumerable.Any(); } diff --git a/src/Core/Configurations/JwtConfigChangeRelay.cs b/src/Core/Configurations/JwtConfigChangeRelay.cs deleted file mode 100644 index 8b34c7a3cf..0000000000 --- a/src/Core/Configurations/JwtConfigChangeRelay.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Azure.DataApiBuilder.Config.ObjectModel; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Validators; -using static NodaTime.TimeZones.ZoneEqualityComparer; - -namespace Azure.DataApiBuilder.Core.Configurations; - -public class JwtConfigChangeRelay -{ - private readonly List _registrations = new(); - private readonly IOptionsMonitor _bearerOptionsMonitor; - private readonly IOptionsMonitorCache _bearerOptionsMonitorCache; - private readonly IPostConfigureOptions[] _postConfigures; - private readonly RuntimeConfigProvider _runtimeConfigProvider; - private readonly List _changeTokenRegistrations; - - public JwtConfigChangeRelay( - IEnumerable> sources, - IOptionsMonitor bearerOptionsMonitor, - IOptionsMonitorCache jwtOptionsMonitorCache, - IEnumerable> jwtPostConfigureOptions, - RuntimeConfigProvider runtimeConfigProvider) - { - _bearerOptionsMonitor = bearerOptionsMonitor; - _bearerOptionsMonitorCache = jwtOptionsMonitorCache; - _postConfigures = jwtPostConfigureOptions as IPostConfigureOptions[] ?? new List>(jwtPostConfigureOptions).ToArray(); - _runtimeConfigProvider = runtimeConfigProvider; - - _changeTokenRegistrations = new List(1) - { - ChangeToken.OnChange(_runtimeConfigProvider.GetChangeToken,PostChangeTokenChangedAction, false) - }; - } - - //https://github.com/dotnet/aspnetcore/issues/52296 - public void ConfigureJwtBearerProvider() - { - AuthenticationOptions? newAuthOptions = _runtimeConfigProvider.GetConfig().Runtime?.Host?.Authentication; - - JwtBearerOptions jwtOptions = new() - { - MapInboundClaims = false, - Audience = newAuthOptions?.Jwt?.Audience, - Authority = newAuthOptions?.Jwt?.Issuer, - TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters() - { - ValidAudience = newAuthOptions?.Jwt?.Audience, - ValidIssuers = new List() { "https://login.microsoftonline.com/291bf275-ea78-4cde-84ea-21309a43a567/v2.0" }, - // Instructs the asp.net core middleware to use the data in the "roles" claim for User.IsInRole() - // See https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsprincipal.isinrole?view=net-6.0#remarks - RoleClaimType = AuthenticationOptions.ROLE_CLAIM_TYPE - } - }; - - //jwtOptions.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); - //jwtOptions.TokenValidationParameters.SignatureValidator = (token, _) => new JsonWebToken(token); - _bearerOptionsMonitorCache.TryRemove(JwtBearerDefaults.AuthenticationScheme); - foreach (IPostConfigureOptions post in _postConfigures) - { - post.PostConfigure(JwtBearerDefaults.AuthenticationScheme, jwtOptions); - } - // _jwtPostConfigureOptions.PostConfigure(JwtBearerDefaults.AuthenticationScheme, jwtOptions); - _bearerOptionsMonitorCache.GetOrAdd(JwtBearerDefaults.AuthenticationScheme, () => jwtOptions); - Console.WriteLine("New Audience: " + _bearerOptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme).Audience); - Console.WriteLine("New Issuer: " + _bearerOptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme).Authority); - - } - - private void PostChangeTokenChangedAction(bool forceUpdate = false) - { - // Skip hot reload circumstances in ProdMode. - if (!forceUpdate && (_runtimeConfigProvider.IsLateConfigured || _runtimeConfigProvider.GetConfig().Runtime?.Host?.Mode == HostMode.Production)) - { - return; - } - //Console.WriteLine("Old Audience: " + jwtOptions.Audience); - JwtBearerOptions jwtOptions = _bearerOptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme); - //Console.WriteLine("Current options: " + jwtOptions.Audience); - AuthenticationOptions? newAuthOptions = _runtimeConfigProvider.GetConfig().Runtime?.Host?.Authentication; - - if (_bearerOptionsMonitorCache.TryRemove(JwtBearerDefaults.AuthenticationScheme)) - { - jwtOptions.Audience = newAuthOptions?.Jwt?.Audience; - jwtOptions.TokenValidationParameters.ValidAudience = newAuthOptions?.Jwt?.Audience; - jwtOptions.Authority = newAuthOptions?.Jwt?.Issuer; - // _jwtPostConfigureOptions.PostConfigure(JwtBearerDefaults.AuthenticationScheme, jwtOptions); - _bearerOptionsMonitorCache.GetOrAdd(JwtBearerDefaults.AuthenticationScheme, () => jwtOptions); - Console.WriteLine("New Audience: " + _bearerOptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme).Audience); - } - } -} diff --git a/src/Core/Configurations/RuntimeConfigProvider.cs b/src/Core/Configurations/RuntimeConfigProvider.cs index 491f3db3cc..12b8dd7b62 100644 --- a/src/Core/Configurations/RuntimeConfigProvider.cs +++ b/src/Core/Configurations/RuntimeConfigProvider.cs @@ -45,7 +45,15 @@ public class RuntimeConfigProvider private RuntimeConfigLoader _configLoader; private DabChangeToken _changeToken = new(); private readonly List? _changeTokenRegistrations; - private JwtConfigChangeRelay? _changeRelay; + + public RuntimeConfigProvider(RuntimeConfigLoader runtimeConfigLoader) + { + _configLoader = runtimeConfigLoader; + _changeTokenRegistrations = new List(1) + { + ChangeToken.OnChange(_configLoader.GetChangeToken, RaiseChanged) + }; + } // Raises the changed event. private void RaiseChanged() @@ -55,7 +63,7 @@ private void RaiseChanged() } /// - /// producer + /// Change Token producer /// /// #pragma warning disable CA1024 // Use properties where appropriate @@ -82,21 +90,6 @@ public string ConfigFilePath } } - public RuntimeConfigProvider(RuntimeConfigLoader runtimeConfigLoader, JwtConfigChangeRelay jwtConfigChangeRelay) - { - _configLoader = runtimeConfigLoader; - _changeRelay = jwtConfigChangeRelay; - _changeTokenRegistrations = new List(1) - { - ChangeToken.OnChange(_configLoader.GetChangeToken, RaiseChanged) - }; - } - - public RuntimeConfigProvider(RuntimeConfigLoader runtimeConfigLoader) - { - _configLoader = runtimeConfigLoader; - } - /// /// Return the previous loaded config, or it will attempt to load the config that /// is known by the loader. @@ -275,6 +268,15 @@ public async Task Initialize( return false; } + /// + /// Runtimeconfig is hot-reloadable when the configuration is not in production mode and not late configured. + /// + /// True when config is hot-reloadable. + public bool IsConfigHotReloadable() + { + return !IsLateConfigured || !(_configLoader.RuntimeConfig?.Runtime?.Host?.Mode == HostMode.Production); + } + private async Task InvokeConfigLoadedHandlersAsync() { List> configLoadedTasks = new(); diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 43ca332a36..4647354e2d 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -20,6 +20,7 @@ using System.Web; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core; using Azure.DataApiBuilder.Core.AuthenticationHelpers; using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Configurations; diff --git a/src/Service.Tests/Configuration/TelemetryTests.cs b/src/Service.Tests/Configuration/TelemetryTests.cs index 1160e94d82..fd1a8f3571 100644 --- a/src/Service.Tests/Configuration/TelemetryTests.cs +++ b/src/Service.Tests/Configuration/TelemetryTests.cs @@ -8,12 +8,12 @@ using System.Net.Http.Json; using System.Threading.Tasks; using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -using Microsoft.IdentityModel.Tokens; using Microsoft.VisualStudio.TestTools.UnitTesting; using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationTests; diff --git a/src/Service/JwtBearerOptionsChangeTokenSource.cs b/src/Service/JwtBearerOptionsChangeTokenSource.cs index bd71853a12..4e202141dc 100644 --- a/src/Service/JwtBearerOptionsChangeTokenSource.cs +++ b/src/Service/JwtBearerOptionsChangeTokenSource.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Core.Configurations; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -11,19 +11,19 @@ namespace Azure.DataApiBuilder.Service; public class JwtBearerOptionsChangeTokenSource : IOptionsChangeTokenSource { //private readonly DabChangeToken _changeToken; - private readonly RuntimeConfigLoader _configLoader; + private readonly RuntimeConfigProvider _configProvider; - public JwtBearerOptionsChangeTokenSource(RuntimeConfigLoader configLoader) + public JwtBearerOptionsChangeTokenSource(RuntimeConfigProvider configProvider) { // Change event source is the provider. - _configLoader = configLoader; + _configProvider = configProvider; } - public string Name => "DogToken"; + public string Name => "Bearer"; public IChangeToken GetChangeToken() { - return _configLoader.GetChangeToken(); + return _configProvider.GetChangeToken(); } } diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 5d098bb97c..4ca775957b 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -88,10 +88,10 @@ public void ConfigureServices(IServiceCollection services) FileSystemRuntimeConfigLoader configLoader = new(fileSystem, hotReloadEventHandler, configFileName, connectionString); RuntimeConfigProvider configProvider = new(configLoader); - services.AddSingleton(); services.AddSingleton(fileSystem); services.AddSingleton(configLoader); services.AddSingleton(configProvider); + if (configProvider.TryGetConfig(out RuntimeConfig? runtimeConfig) && runtimeConfig.Runtime?.Telemetry?.ApplicationInsights is not null && runtimeConfig.Runtime.Telemetry.ApplicationInsights.Enabled) @@ -175,7 +175,16 @@ public void ConfigureServices(IServiceCollection services) //Enable accessing HttpContext in RestService to get ClaimsPrincipal. services.AddHttpContextAccessor(); - ConfigureAuthV2(services, configLoader); + // Use HotReload aware authentication configuration when in development mode + // and when runtime config was provided at startup. + if (runtimeConfig is not null && runtimeConfig.Runtime?.Host?.Mode is HostMode.Development) + { + ConfigureAuthV2(services, configProvider); + } + else + { + ConfigureAuthentication(services, configProvider); + } services.AddAuthorization(); services.AddSingleton>(implementationFactory: (serviceProvider) => @@ -529,15 +538,13 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP /// Wires up all DAB supported authentication providers. DAB uses the user configured /// authentcation provider to determine which provider to use for authentication at request time. /// - private void ConfigureAuthV2(IServiceCollection services, RuntimeConfigLoader configLoader) + private static void ConfigureAuthV2(IServiceCollection services, RuntimeConfigProvider runtimeConfigProvider) { - services.AddSingleton>(new JwtBearerOptionsChangeTokenSource(configLoader)); - services.AddSingleton>(new ConfigurationChangeTokenSource(JwtBearerDefaults.AuthenticationScheme, Configuration)); - //services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerPostConfigureOptions>()); - // Purposely + services.AddSingleton>(new JwtBearerOptionsChangeTokenSource(runtimeConfigProvider)); + services.AddSingleton, ConfigureJwtBearerOptions>(); services.AddAuthentication() .AddEnvDetectedEasyAuth() - .AddJwtBearer() + .AddJwtBearer(authenticationScheme: "Bearer") .AddSimulatorAuthentication(); } @@ -623,8 +630,12 @@ private async Task PerformOnConfigChangeAsync(IApplicationBuilder app) { // Running only in developer mode to ensure fast and smooth startup in production. runtimeConfigValidator.ValidatePermissionsInConfig(runtimeConfig); - JwtConfigChangeRelay relay = app.ApplicationServices.GetService()!; - relay.ConfigureJwtBearerProvider(); + + if (!runtimeConfigProvider.IsLateConfigured) + { + //JwtConfigChangeRelay relay = app.ApplicationServices.GetService()!; + //relay.ConfigureJwtBearerProvider(); + } } IMetadataProviderFactory sqlMetadataProviderFactory = From 8e33fefdf534f0b80f77e5b57b5e19485fb4861f Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Thu, 10 Oct 2024 14:41:20 -0700 Subject: [PATCH 3/8] Added comments. --- src/Cli/CollectionUtilities.cs | 3 ++ src/Config/DabChangeToken.cs | 20 ++++++++++++ src/Config/RuntimeConfigLoader.cs | 17 ++++++++-- ...lientRoleHeaderAuthenticationMiddleware.cs | 17 ++++++++-- .../ConfigureJwtBearerOptions.cs | 19 ++++++++++-- ...EasyAuthAuthenticationBuilderExtensions.cs | 7 ++++- .../EasyAuthAuthenticationDefaults.cs | 16 +++++++--- .../GenericOAuthDefaults.cs | 3 ++ src/Core/CollectionUtilities.cs | 3 ++ .../Configurations/RuntimeConfigProvider.cs | 14 +++++++-- .../Controllers/ConfigurationController.cs | 31 +------------------ .../JwtBearerOptionsChangeTokenSource.cs | 19 ++++++++++-- src/Service/Startup.cs | 13 +++----- 13 files changed, 126 insertions(+), 56 deletions(-) diff --git a/src/Cli/CollectionUtilities.cs b/src/Cli/CollectionUtilities.cs index 6fd0b606dc..9f1eb62097 100644 --- a/src/Cli/CollectionUtilities.cs +++ b/src/Cli/CollectionUtilities.cs @@ -5,7 +5,10 @@ namespace Cli; /// /// A class which contains useful methods for processing collections. +/// Pulled from Microsoft.IdentityModel.JsonWebTokens which changed the +/// helper to be internal. /// +/// internal static class CollectionUtilities { /// diff --git a/src/Config/DabChangeToken.cs b/src/Config/DabChangeToken.cs index 2311e34130..a6ba4755e6 100644 --- a/src/Config/DabChangeToken.cs +++ b/src/Config/DabChangeToken.cs @@ -5,14 +5,34 @@ namespace Azure.DataApiBuilder.Config; +/// +/// Propagates notifications that a change has occurred. +/// +/// public class DabChangeToken : IChangeToken { private CancellationTokenSource _cts = new(); + /// + /// Gets a value that indicates if a change has occurred. + /// public bool HasChanged => _cts.IsCancellationRequested; + /// + /// Indicates if this token will pro-actively raise callbacks. If false, the token consumer must + /// poll to detect changes. + /// public bool ActiveChangeCallbacks => true; + /// + /// Registers for a callback that will be invoked when the entry has changed. + /// MUST be set before the callback is invoked. + /// Used by ChangeToken.OnChange callback registration. + /// + /// The to invoke. + /// State to be passed into the callback. + /// An that is used to unregister the callback. + /// public IDisposable RegisterChangeCallback(Action callback, object? state) { return _cts.Token.Register(callback, state); diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index c6deb140dd..d75a4527cc 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -38,7 +38,10 @@ public RuntimeConfigLoader(HotReloadEventHandler? handler = _connectionString = connectionString; } - // Signals changes to the config to listeners. + /// + /// Change token producer which returns an uncancelled/unsignalled change token. + /// + /// DabChangeToken #pragma warning disable CA1024 // Use properties where appropriate public IChangeToken GetChangeToken() #pragma warning restore CA1024 // Use properties where appropriate @@ -46,7 +49,15 @@ public IChangeToken GetChangeToken() return _changeToken; } - // Raises the changed event. + /// + /// Swaps out the old change token with a new change token and + /// signals that a change has occurred. + /// + /// + /// Example usage of Interlocked.Exchange(...) to refresh change token. + /// + /// Sets a variable to a specified value as an atomic operation. + /// private void RaiseChanged() { DabChangeToken previousToken = Interlocked.Exchange(ref _changeToken, new DabChangeToken()); @@ -64,6 +75,8 @@ public void SendEventNotification(string message = "") { HotReloadEventArgs args = new(message); DocumentorOnConfigChanged(args); + + // Signal that a change has occurred to all change token listeners. RaiseChanged(); } diff --git a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs index 8887865501..2955ca5a52 100644 --- a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs +++ b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs @@ -59,12 +59,16 @@ public ClientRoleHeaderAuthenticationMiddleware(RequestDelegate next, /// Request metadata public async Task InvokeAsync(HttpContext httpContext) { + // Determine the authentication scheme to use based on dab-config.json. + // Compatible with both ConfigureAuthentication and ConfigureAuthenticationV2 in startup.cs. + // This means that this code is resilient to whether or not the default authentication scheme is set in startup. + AuthenticationOptions? dabAuthNOptions = _runtimeConfigProvider.GetConfig().Runtime?.Host?.Authentication; + string scheme = ResolveConfiguredAuthNScheme(dabAuthNOptions?.Provider); + // authNResult will be one of: // 1. Succeeded - Authenticated // 2. Failure - Token issue // 3. None - No token provided, no auth result. - AuthenticationOptions? dabAuthNOptions = _runtimeConfigProvider.GetConfig().Runtime?.Host?.Authentication; - string scheme = ResolveConfiguredAuthNScheme(dabAuthNOptions?.Provider); AuthenticateResult authNResult = await httpContext.AuthenticateAsync(scheme); // Reject and terminate the request when an invalid token is provided @@ -163,6 +167,12 @@ public static bool IsSystemRole(string roleName) roleName.Equals(AuthorizationType.Anonymous.ToString(), StringComparison.OrdinalIgnoreCase); } + /// + /// Uses the dab-config.json's Authentication provider name to resolve the + /// authentication scheme to use with httpContext.AuthenticateAsync(scheme). + /// + /// Dab config defined authentication provider name. + /// Authentication Scheme private static string ResolveConfiguredAuthNScheme(string? configuredProviderName) { switch (configuredProviderName) @@ -178,6 +188,9 @@ private static string ResolveConfiguredAuthNScheme(string? configuredProviderNam return JwtBearerDefaults.AuthenticationScheme; case "Custom": default: + // Changing this value is a breaking change because non-out of box + // authentication provider names supplied in dab-config.json indicate + // that JWT bearer authentication should be used. return GenericOAuthDefaults.AUTHENTICATIONSCHEME; } } diff --git a/src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs b/src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs index 4f05e6f804..0858313146 100644 --- a/src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs +++ b/src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs @@ -9,21 +9,31 @@ namespace Azure.DataApiBuilder.Service; +/// +/// Named options configuration for JwtBearerOptions. +/// public class ConfigureJwtBearerOptions : IConfigureNamedOptions { private readonly RuntimeConfigProvider _runtimeConfigProvider; + + /// + /// By registering this instance of IConfigureNamedOptions, the internal + /// .NET OptionsFactory will call Configure(string? name, JwtBearerOptions options) + /// when JwtBearerOptions is requested and fetch the latest configuration + /// from the RuntimeConfigProvider. + /// + /// Source of latest configuration. public ConfigureJwtBearerOptions(RuntimeConfigProvider runtimeConfigProvider) { _runtimeConfigProvider = runtimeConfigProvider; } - // Configure the named instance + // JwtBearerOptions configuration. Returned to OptionsFactory. public void Configure(string? name, JwtBearerOptions options) { - // Skip JwtBearerOptions hot-reload circumstances in ProdMode. + // Don't refresh authentication config when hot reload is disabled. if (!_runtimeConfigProvider.IsConfigHotReloadable()) { - // perhaps have this check in the change token ? return; } @@ -45,11 +55,14 @@ public void Configure(string? name, JwtBearerOptions options) ValidIssuer = newAuthOptions.Jwt.Issuer, // Instructs the asp.net core middleware to use the data in the "roles" claim for User.IsInRole() // See https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsprincipal.isinrole?view=net-6.0#remarks + // This should eventually be configurable to address #2395 RoleClaimType = AuthenticationOptions.ROLE_CLAIM_TYPE }; if (newAuthOptions.Provider.Equals("AzureAD") || newAuthOptions.Provider.Equals("EntraID")) { + // Enables the validation of the issuer of the signing keys + // used by the Microsoft identity platform (AAD) against the issuer of the token. options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); } } diff --git a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs index 5c835f33a4..b97bcf39dc 100644 --- a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs +++ b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs @@ -44,11 +44,16 @@ public static AuthenticationBuilder AddEasyAuthAuthentication( return builder; } + /// + /// Used for ConfigureAuthenticationV2() where all EasyAuth schemes are registered. + /// This function doesn't register EasyAuthType.AppService if the AppService environment is not detected. + /// + /// public static AuthenticationBuilder AddEnvDetectedEasyAuth(this AuthenticationBuilder builder) { if (builder is null) { - throw new System.ArgumentNullException(nameof(builder)); + throw new ArgumentNullException(nameof(builder)); } builder.AddScheme( diff --git a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs index f44bd47d36..fb3293fe50 100644 --- a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs +++ b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs @@ -4,21 +4,27 @@ namespace Azure.DataApiBuilder.Core.AuthenticationHelpers; /// -/// Default values related to EasyAuthAuthentication handler. +/// EasyAuth authentication scheme names granularized by provider +/// to enable compatility with HotReloading authentication settings. +/// Authentication schemes: +/// - Correlate to an authentication handler +/// - Indicate to AuthenticateAsync which handler to use /// +/// public static class EasyAuthAuthenticationDefaults { /// + /// Used in ConfigureAuthentication() (AuthV1) /// The default value used for EasyAuthAuthenticationOptions.AuthenticationScheme. /// public const string AUTHENTICATIONSCHEME = "EasyAuthAuthentication"; - /// - /// EasyAuth authentication scheme names granularized by provider - /// to enable compatility with HotReloading authentication settings. - /// public const string SWAAUTHSCHEME = "StaticWebAppsAuthentication"; + public const string APPSERVICEAUTHSCHEME = "AppServiceAuthentication"; + /// + /// Warning message emitted when the EasyAuth payload is invalid. + /// public const string INVALID_PAYLOAD_ERROR = "Invalid EasyAuth Payload."; } diff --git a/src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs b/src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs index 29569f7b91..0faf2b3085 100644 --- a/src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs +++ b/src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs @@ -3,6 +3,9 @@ namespace Azure.DataApiBuilder.Core.AuthenticationHelpers; +/// +/// Authentication Scheme name for generic OAuth providers. +/// public class GenericOAuthDefaults { public const string AUTHENTICATIONSCHEME = "OAuthAuthentication"; diff --git a/src/Core/CollectionUtilities.cs b/src/Core/CollectionUtilities.cs index 12d123bb07..3d94df33c9 100644 --- a/src/Core/CollectionUtilities.cs +++ b/src/Core/CollectionUtilities.cs @@ -5,7 +5,10 @@ namespace Azure.DataApiBuilder.Core; /// /// A class which contains useful methods for processing collections. +/// Pulled from Microsoft.IdentityModel.JsonWebTokens which changed the +/// helper to be internal. /// +/// internal static class CollectionUtilities { /// diff --git a/src/Core/Configurations/RuntimeConfigProvider.cs b/src/Core/Configurations/RuntimeConfigProvider.cs index 12b8dd7b62..6738ed025a 100644 --- a/src/Core/Configurations/RuntimeConfigProvider.cs +++ b/src/Core/Configurations/RuntimeConfigProvider.cs @@ -55,7 +55,15 @@ public RuntimeConfigProvider(RuntimeConfigLoader runtimeConfigLoader) }; } - // Raises the changed event. + /// + /// Swaps out the old change token with a new change token and + /// signals that a change has occurred. + /// + /// + /// Example usage of Interlocked.Exchange(...) to refresh change token. + /// + /// Sets a variable to a specified value as an atomic operation. + /// private void RaiseChanged() { DabChangeToken previousToken = Interlocked.Exchange(ref _changeToken, new DabChangeToken()); @@ -63,9 +71,9 @@ private void RaiseChanged() } /// - /// Change Token producer + /// Change token producer which returns an uncancelled/unsignalled change token. /// - /// + /// DabChangeToken #pragma warning disable CA1024 // Use properties where appropriate public IChangeToken GetChangeToken() #pragma warning restore CA1024 // Use properties where appropriate diff --git a/src/Service/Controllers/ConfigurationController.cs b/src/Service/Controllers/ConfigurationController.cs index 085fdcf7a3..be3f9bd727 100644 --- a/src/Service/Controllers/ConfigurationController.cs +++ b/src/Service/Controllers/ConfigurationController.cs @@ -6,9 +6,7 @@ using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Core.Models; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Azure.DataApiBuilder.Service.Controllers @@ -19,11 +17,9 @@ public class ConfigurationController : Controller { RuntimeConfigProvider _configurationProvider; private readonly ILogger _logger; - private readonly IConfiguration _configuration; - public ConfigurationController(RuntimeConfigProvider configurationProvider, ILogger logger, IConfiguration configuration) + public ConfigurationController(RuntimeConfigProvider configurationProvider, ILogger logger) { - _configuration = configuration; _configurationProvider = configurationProvider; _logger = logger; } @@ -74,31 +70,6 @@ public async Task Index([FromBody] ConfigurationPostParametersV2 c return BadRequest(); } - [HttpPost("changeJwtProvider")] - public ActionResult ChangeJwtProvider([FromBody] JwtConfigPostParameters jwtConfig) - { - try - { - _configuration.GetSection("JwtBearer"); - _configuration["Authentication:Schemes:Bearer:ValidAudiences"] = jwtConfig.Audience; - } - catch (Exception e) - { - _logger.LogError( - exception: e, - message: "{correlationId} Exception during configuration initialization.", - HttpContextExtensions.GetLoggerCorrelationId(HttpContext)); - } - - return BadRequest(); - } - - public record class JwtConfigPostParameters( - string SchemeName, - string Audience, - string Authority) - { } - /// /// Takes in the runtime configuration, schema, connection string and access token and configures the runtime. /// If the runtime is already configured, it will return a conflict result. diff --git a/src/Service/JwtBearerOptionsChangeTokenSource.cs b/src/Service/JwtBearerOptionsChangeTokenSource.cs index 4e202141dc..9c9e6afb94 100644 --- a/src/Service/JwtBearerOptionsChangeTokenSource.cs +++ b/src/Service/JwtBearerOptionsChangeTokenSource.cs @@ -8,19 +8,34 @@ namespace Azure.DataApiBuilder.Service; +/// +/// Used by IOptionsMonitor to register a change token for JwtBearerOptions. +/// When DAB gets a new runtimeconfig via hot-reload, DAB will signal +/// (via RuntimeConfigLoader -> RuntimeConfigProvider -> JwtBearerOptionsChangeTokenSource) +/// that a change has occurred and IOptionsMonitor will reload the JwtBearerOptions. +/// +/// public class JwtBearerOptionsChangeTokenSource : IOptionsChangeTokenSource { - //private readonly DabChangeToken _changeToken; private readonly RuntimeConfigProvider _configProvider; + /// + /// Get RuntimeConfigProvider to use as the change event source. + /// + /// Change token source. public JwtBearerOptionsChangeTokenSource(RuntimeConfigProvider configProvider) { - // Change event source is the provider. _configProvider = configProvider; } public string Name => "Bearer"; + /// + /// Returns a change token that signals when the JwtBearerOptions should be reloaded. + /// Used by ChangeToken.OnChange to register a callback when the change token signals. + /// + /// + /// DabChangeToken public IChangeToken GetChangeToken() { return _configProvider.GetChangeToken(); diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 4ca775957b..40f6943a7d 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -179,7 +179,7 @@ public void ConfigureServices(IServiceCollection services) // and when runtime config was provided at startup. if (runtimeConfig is not null && runtimeConfig.Runtime?.Host?.Mode is HostMode.Development) { - ConfigureAuthV2(services, configProvider); + ConfigureAuthenticationV2(services, configProvider); } else { @@ -537,8 +537,11 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP /// /// Wires up all DAB supported authentication providers. DAB uses the user configured /// authentcation provider to determine which provider to use for authentication at request time. + /// JwtBearerOptions are hydrated when ConfigureJwtBearerOptions is called by OptionsFactory internally by .NET. /// - private static void ConfigureAuthV2(IServiceCollection services, RuntimeConfigProvider runtimeConfigProvider) + /// Guidance for registering IOptionsChangeTokenSource + /// Guidance for registering named options. + private static void ConfigureAuthenticationV2(IServiceCollection services, RuntimeConfigProvider runtimeConfigProvider) { services.AddSingleton>(new JwtBearerOptionsChangeTokenSource(runtimeConfigProvider)); services.AddSingleton, ConfigureJwtBearerOptions>(); @@ -630,12 +633,6 @@ private async Task PerformOnConfigChangeAsync(IApplicationBuilder app) { // Running only in developer mode to ensure fast and smooth startup in production. runtimeConfigValidator.ValidatePermissionsInConfig(runtimeConfig); - - if (!runtimeConfigProvider.IsLateConfigured) - { - //JwtConfigChangeRelay relay = app.ApplicationServices.GetService()!; - //relay.ConfigureJwtBearerProvider(); - } } IMetadataProviderFactory sqlMetadataProviderFactory = From 4d37450568f24c9f3a1485d31c0d8df04ecc720b Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Thu, 10 Oct 2024 14:46:59 -0700 Subject: [PATCH 4/8] Undo remove multi targetting. --- src/Auth/Azure.DataApiBuilder.Auth.csproj | 2 +- src/Cli.Tests/Cli.Tests.csproj | 2 +- src/Cli/Cli.csproj | 2 +- src/Config/Azure.DataApiBuilder.Config.csproj | 2 +- src/Core/Azure.DataApiBuilder.Core.csproj | 2 +- src/Directory.Packages.props | 11 ++++++++++- src/Product/Azure.DataApiBuilder.Product.csproj | 2 +- ...Azure.DataApiBuilder.Service.GraphQLBuilder.csproj | 2 +- .../Azure.DataApiBuilder.Service.Tests.csproj | 2 +- src/Service/Azure.DataApiBuilder.Service.csproj | 2 +- src/Service/Startup.cs | 2 +- 11 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/Auth/Azure.DataApiBuilder.Auth.csproj b/src/Auth/Azure.DataApiBuilder.Auth.csproj index 1ee2df57b8..eb677830e2 100644 --- a/src/Auth/Azure.DataApiBuilder.Auth.csproj +++ b/src/Auth/Azure.DataApiBuilder.Auth.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0;net6.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Cli.Tests/Cli.Tests.csproj b/src/Cli.Tests/Cli.Tests.csproj index a8081f5250..46192e3802 100644 --- a/src/Cli.Tests/Cli.Tests.csproj +++ b/src/Cli.Tests/Cli.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0;net6.0 enable enable false diff --git a/src/Cli/Cli.csproj b/src/Cli/Cli.csproj index 5321f52356..e7afbd74ff 100644 --- a/src/Cli/Cli.csproj +++ b/src/Cli/Cli.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net8.0;net6.0 Cli enable enable diff --git a/src/Config/Azure.DataApiBuilder.Config.csproj b/src/Config/Azure.DataApiBuilder.Config.csproj index 735c88cc16..f28ed113d4 100644 --- a/src/Config/Azure.DataApiBuilder.Config.csproj +++ b/src/Config/Azure.DataApiBuilder.Config.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0;net6.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Core/Azure.DataApiBuilder.Core.csproj b/src/Core/Azure.DataApiBuilder.Core.csproj index 9c1ce9aeb8..f2c939151f 100644 --- a/src/Core/Azure.DataApiBuilder.Core.csproj +++ b/src/Core/Azure.DataApiBuilder.Core.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0;net6.0 enable enable true diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 35041fa787..e9a7b41c62 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -61,4 +61,13 @@ - \ No newline at end of file + + + + + + + + + + diff --git a/src/Product/Azure.DataApiBuilder.Product.csproj b/src/Product/Azure.DataApiBuilder.Product.csproj index 6045200487..825c66f315 100644 --- a/src/Product/Azure.DataApiBuilder.Product.csproj +++ b/src/Product/Azure.DataApiBuilder.Product.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0;net6.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj b/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj index 87eab3c75b..637ff065c9 100644 --- a/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj +++ b/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0;net6.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj b/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj index 8badb7d90d..b040c0453f 100644 --- a/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj +++ b/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0;net6.0 false disable $(BaseOutputPath)\tests diff --git a/src/Service/Azure.DataApiBuilder.Service.csproj b/src/Service/Azure.DataApiBuilder.Service.csproj index 5cb3be73b2..710048b9fa 100644 --- a/src/Service/Azure.DataApiBuilder.Service.csproj +++ b/src/Service/Azure.DataApiBuilder.Service.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0;net6.0 Debug;Release;Docker $(BaseOutputPath)\engine win-x64;linux-x64;osx-x64 diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 40f6943a7d..4f95fb0315 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -547,7 +547,7 @@ private static void ConfigureAuthenticationV2(IServiceCollection services, Runti services.AddSingleton, ConfigureJwtBearerOptions>(); services.AddAuthentication() .AddEnvDetectedEasyAuth() - .AddJwtBearer(authenticationScheme: "Bearer") + .AddJwtBearer() .AddSimulatorAuthentication(); } From b3ea16c2c65ef5cfa701ccd200b9f905d35b94b1 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Thu, 10 Oct 2024 14:50:38 -0700 Subject: [PATCH 5/8] remove unintended spacing. --- src/Auth/Azure.DataApiBuilder.Auth.csproj | 2 +- src/Cli/Cli.csproj | 2 +- src/Config/Azure.DataApiBuilder.Config.csproj | 2 +- src/Core/Azure.DataApiBuilder.Core.csproj | 2 +- src/Core/Resolvers/MsSqlQueryBuilder.cs | 1 - src/Product/Azure.DataApiBuilder.Product.csproj | 2 +- .../Azure.DataApiBuilder.Service.GraphQLBuilder.csproj | 2 +- src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj | 2 +- 8 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Auth/Azure.DataApiBuilder.Auth.csproj b/src/Auth/Azure.DataApiBuilder.Auth.csproj index eb677830e2..9f63cd3ed6 100644 --- a/src/Auth/Azure.DataApiBuilder.Auth.csproj +++ b/src/Auth/Azure.DataApiBuilder.Auth.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0;net6.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Cli/Cli.csproj b/src/Cli/Cli.csproj index e7afbd74ff..fec0cc9786 100644 --- a/src/Cli/Cli.csproj +++ b/src/Cli/Cli.csproj @@ -2,7 +2,7 @@ Exe - net8.0;net6.0 + net8.0;net6.0 Cli enable enable diff --git a/src/Config/Azure.DataApiBuilder.Config.csproj b/src/Config/Azure.DataApiBuilder.Config.csproj index f28ed113d4..ad0bca3ef4 100644 --- a/src/Config/Azure.DataApiBuilder.Config.csproj +++ b/src/Config/Azure.DataApiBuilder.Config.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0;net6.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Core/Azure.DataApiBuilder.Core.csproj b/src/Core/Azure.DataApiBuilder.Core.csproj index f2c939151f..60b749c7e4 100644 --- a/src/Core/Azure.DataApiBuilder.Core.csproj +++ b/src/Core/Azure.DataApiBuilder.Core.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0;net6.0 enable enable true diff --git a/src/Core/Resolvers/MsSqlQueryBuilder.cs b/src/Core/Resolvers/MsSqlQueryBuilder.cs index 43e7fd775f..93662e7d2b 100644 --- a/src/Core/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Core/Resolvers/MsSqlQueryBuilder.cs @@ -7,7 +7,6 @@ using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Models; using Microsoft.Data.SqlClient; -using Microsoft.IdentityModel.Tokens; namespace Azure.DataApiBuilder.Core.Resolvers { diff --git a/src/Product/Azure.DataApiBuilder.Product.csproj b/src/Product/Azure.DataApiBuilder.Product.csproj index 825c66f315..f0d0e70927 100644 --- a/src/Product/Azure.DataApiBuilder.Product.csproj +++ b/src/Product/Azure.DataApiBuilder.Product.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0;net6.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj b/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj index 637ff065c9..649e46c550 100644 --- a/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj +++ b/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0;net6.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj b/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj index b040c0453f..14722aea7d 100644 --- a/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj +++ b/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 + net8.0;net6.0 false disable $(BaseOutputPath)\tests From 7bd8a0587f870fa753aacb05c0ba547bc11e9519 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Fri, 11 Oct 2024 15:12:15 -0700 Subject: [PATCH 6/8] fix unit test bugs by correctly wiring up authN and runtimeconfigprovider stub with authN settings populated now that ClientRoleHeaderAuthenticationMiddleware queries the runtimeconfigprovider for every request to determine authentication scheme to authenticate with. --- ...EasyAuthAuthenticationBuilderExtensions.cs | 29 +++++++++++------ .../EasyAuthAuthenticationHandler.cs | 17 +++++++++- .../Helpers/RuntimeConfigAuthHelper.cs | 29 +++++++++++++++++ .../Helpers/WebHostBuilderHelper.cs | 32 +++++++++++++------ .../JwtTokenAuthenticationUnitTests.cs | 12 +++++-- .../SimulatorAuthenticationUnitTests.cs | 7 ++-- .../Configuration/ConfigurationTests.cs | 2 +- 7 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 src/Service.Tests/Authentication/Helpers/RuntimeConfigAuthHelper.cs diff --git a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs index b97bcf39dc..32846bff51 100644 --- a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs +++ b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs @@ -27,20 +27,29 @@ public static AuthenticationBuilder AddEasyAuthAuthentication( throw new System.ArgumentNullException(nameof(builder)); } - builder.AddScheme( - authenticationScheme: EasyAuthAuthenticationDefaults.AUTHENTICATIONSCHEME, - displayName: EasyAuthAuthenticationDefaults.AUTHENTICATIONSCHEME, - options => - { - if (easyAuthAuthenticationProvider is EasyAuthType.StaticWebApps) + if (easyAuthAuthenticationProvider is EasyAuthType.StaticWebApps) + { + //builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, EasyAuthConfigureOptions>()); + + builder.AddScheme( + authenticationScheme: EasyAuthAuthenticationDefaults.SWAAUTHSCHEME, + displayName: EasyAuthAuthenticationDefaults.SWAAUTHSCHEME, + options => { options.EasyAuthProvider = EasyAuthType.StaticWebApps; - } - else if (easyAuthAuthenticationProvider is EasyAuthType.AppService) + }); + } + else + { + builder.AddScheme( + authenticationScheme: EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME, + displayName: EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME, + options => { options.EasyAuthProvider = EasyAuthType.AppService; - } - }); + }); + } + return builder; } diff --git a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs index 4ce45a0543..156a40a304 100644 --- a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs +++ b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs @@ -67,7 +67,22 @@ protected override Task HandleAuthenticateAsync() { if (Context.Request.Headers[AuthenticationOptions.CLIENT_PRINCIPAL_HEADER].Count > 0) { - ClaimsIdentity? identity = OptionsMonitor.CurrentValue.EasyAuthProvider switch + // 'Options' is hydrated using named options (authentication scheme name) from the internal OptionsMonitor + // when the base class AuthenticationHandler::InitializeAsync() method + // is invoked -> which is once for every request. + // - We shouldn't use OptionsMonitor.CurrentValue.EasyAuthProvider because there exists a default + // EasyAuthAuthenticationOptions instance implicitly registered in the DI container which is set as "currentValue." + // The default EasyAuthAuthenticationOptions instance resolves the default EasyAuth enum value StaticWebApps + // which prevents AppService authentication from working when configured. + // The OptionsMonitorCache contains two options registrations: + // 1. Named (the one we want as configured in startup.cs) + // 2. Unnamed Default (we don't want this one). + // We could remove the unnamed option from the OptionsMonitorCache by overridding the base class (essentially a NOOP) + // function InitializeHandlerAsync (aspnetcore issue 57393), though that is unneeded by accessing options as done below. + // https://github.com/dotnet/aspnetcore/blob/v8.0.10/src/Security/Authentication/Core/src/AuthenticationHandler.cs#L155 + // https://github.com/dotnet/aspnetcore/issues/17539 + // https://github.com/dotnet/aspnetcore/issues/57393#issuecomment-2296992453 + ClaimsIdentity? identity = Options.EasyAuthProvider switch { EasyAuthType.StaticWebApps => StaticWebAppsAuthentication.Parse(Context, Logger), EasyAuthType.AppService => AppServiceAuthentication.Parse(Context, Logger), diff --git a/src/Service.Tests/Authentication/Helpers/RuntimeConfigAuthHelper.cs b/src/Service.Tests/Authentication/Helpers/RuntimeConfigAuthHelper.cs new file mode 100644 index 0000000000..12c7db4fce --- /dev/null +++ b/src/Service.Tests/Authentication/Helpers/RuntimeConfigAuthHelper.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Service.Tests.Authentication.Helpers; + +internal static class RuntimeConfigAuthHelper +{ + internal static RuntimeConfig CreateTestConfigWithAuthNProvider(AuthenticationOptions authenticationOptions) + { + DataSource dataSource = new(DatabaseType.MSSQL, "", new()); + + Config.ObjectModel.HostOptions hostOptions = new(Cors: null, Authentication: authenticationOptions); + RuntimeConfig config = new( + Schema: FileSystemRuntimeConfigLoader.SCHEMA, + DataSource: dataSource, + Runtime: new( + Rest: new(), + GraphQL: new(), + Host: hostOptions + ), + Entities: new(new Dictionary()) + ); + return config; + } +} diff --git a/src/Service.Tests/Authentication/Helpers/WebHostBuilderHelper.cs b/src/Service.Tests/Authentication/Helpers/WebHostBuilderHelper.cs index f1685bcaec..5a4e2fae82 100644 --- a/src/Service.Tests/Authentication/Helpers/WebHostBuilderHelper.cs +++ b/src/Service.Tests/Authentication/Helpers/WebHostBuilderHelper.cs @@ -41,17 +41,24 @@ public static class WebHostBuilderHelper /// - DAB's Simulator/ EasyAuth authentication middleware and ClientRoleHeader middleware /// - dotnet's authorization middleware. /// - /// Runtime configured identity provider name. + /// Runtime configured identity provider name. This is different that authentication scheme name because + /// the configured value is simpler. /// Whether to include authorization middleware in request pipeline. /// IHost to be used to create a TestServer public static async Task CreateWebHost( string provider, bool useAuthorizationMiddleware) { - // Setup RuntimeConfigProvider object for the pipeline. MockFileSystem fileSystem = new(); - FileSystemRuntimeConfigLoader loader = new(fileSystem); - RuntimeConfigProvider runtimeConfigProvider = new(loader); + FileSystemRuntimeConfigLoader fileSystemRuntimeConfigLoader = new(new MockFileSystem()); + AuthenticationOptions authOptions = new() + { + Provider = provider + }; + + RuntimeConfig runtimeConfig = RuntimeConfigAuthHelper.CreateTestConfigWithAuthNProvider(authOptions); + fileSystemRuntimeConfigLoader.RuntimeConfig = runtimeConfig; + RuntimeConfigProvider runtimeConfigProvider = new(fileSystemRuntimeConfigLoader); return await new HostBuilder() .ConfigureWebHost(webBuilder => @@ -60,7 +67,7 @@ public static async Task CreateWebHost( .UseTestServer() .ConfigureServices(services => { - if (string.Equals(provider, SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(provider, AuthenticationOptions.SIMULATOR_AUTHENTICATION, StringComparison.OrdinalIgnoreCase)) { services.AddAuthentication(defaultScheme: SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME) .AddSimulatorAuthentication(); @@ -68,7 +75,7 @@ public static async Task CreateWebHost( else { EasyAuthType easyAuthProvider = (EasyAuthType)Enum.Parse(typeof(EasyAuthType), provider, ignoreCase: true); - services.AddAuthentication(defaultScheme: EasyAuthAuthenticationDefaults.AUTHENTICATIONSCHEME) + services.AddAuthentication() .AddEasyAuthAuthentication(easyAuthProvider); } @@ -125,10 +132,17 @@ public static async Task CreateWebHost( /// IHost to be used to create a TestServer public static async Task CreateWebHostCustomIssuer(SecurityKey key) { - // Setup RuntimeConfigProvider object for the pipeline. MockFileSystem fileSystem = new(); - FileSystemRuntimeConfigLoader loader = new(fileSystem); - RuntimeConfigProvider runtimeConfigProvider = new(loader); + FileSystemRuntimeConfigLoader fileSystemRuntimeConfigLoader = new(new MockFileSystem()); + AuthenticationOptions authOptions = new() + { + Provider = "AzureAD", + Jwt = new(Audience: AUDIENCE, Issuer: LOCAL_ISSUER) + }; + + RuntimeConfig runtimeConfig = RuntimeConfigAuthHelper.CreateTestConfigWithAuthNProvider(authOptions); + fileSystemRuntimeConfigLoader.RuntimeConfig = runtimeConfig; + RuntimeConfigProvider runtimeConfigProvider = new(fileSystemRuntimeConfigLoader); return await new HostBuilder() .ConfigureWebHost(webBuilder => diff --git a/src/Service.Tests/Authentication/JwtTokenAuthenticationUnitTests.cs b/src/Service.Tests/Authentication/JwtTokenAuthenticationUnitTests.cs index 66f26df0b4..a805c3ab1a 100644 --- a/src/Service.Tests/Authentication/JwtTokenAuthenticationUnitTests.cs +++ b/src/Service.Tests/Authentication/JwtTokenAuthenticationUnitTests.cs @@ -14,6 +14,7 @@ using Azure.DataApiBuilder.Core.AuthenticationHelpers; using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Service.Tests.Authentication.Helpers; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -298,8 +299,15 @@ private static async Task CreateWebHostCustomIssuer(SecurityKey key) { // Setup RuntimeConfigProvider object for the pipeline. MockFileSystem fileSystem = new(); - FileSystemRuntimeConfigLoader loader = new(fileSystem); - RuntimeConfigProvider runtimeConfigProvider = new(loader); + FileSystemRuntimeConfigLoader fileSystemRuntimeConfigLoader = new(new MockFileSystem()); + AuthenticationOptions authOptions = new() + { + Provider = "AzureAD" + }; + + RuntimeConfig runtimeConfig = RuntimeConfigAuthHelper.CreateTestConfigWithAuthNProvider(authOptions); + fileSystemRuntimeConfigLoader.RuntimeConfig = runtimeConfig; + RuntimeConfigProvider runtimeConfigProvider = new(fileSystemRuntimeConfigLoader); return await new HostBuilder() .ConfigureWebHost(webBuilder => diff --git a/src/Service.Tests/Authentication/SimulatorAuthenticationUnitTests.cs b/src/Service.Tests/Authentication/SimulatorAuthenticationUnitTests.cs index 907b7d9f21..6165d650a9 100644 --- a/src/Service.Tests/Authentication/SimulatorAuthenticationUnitTests.cs +++ b/src/Service.Tests/Authentication/SimulatorAuthenticationUnitTests.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Net; using System.Threading.Tasks; -using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator; +using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Service.Tests.Authentication.Helpers; using Microsoft.AspNetCore.Http; @@ -55,15 +55,14 @@ public async Task TestAuthenticatedRequestInDevelopmentMode(string clientRoleHea #region Helper Methods /// /// Creates the TestServer with the minimum middleware setup necessary to - /// test EasyAuth authentication mechanisms. + /// test the "Simulator" authentication provider's authentication mechanisms. /// Sends a request with a clientRoleHeader to the TestServer created. /// /// Name of role to include in header. - /// public static async Task SendRequestAndGetHttpContextState(string? clientRole = null) { using IHost host = await WebHostBuilderHelper.CreateWebHost( - provider: SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME, + provider: AuthenticationOptions.SIMULATOR_AUTHENTICATION, useAuthorizationMiddleware: true); TestServer server = host.GetTestServer(); diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 4647354e2d..196d4bfe6e 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -1836,7 +1836,7 @@ public void TestGetConfigFileNameForEnvironment( /// The expected phrase in the response body. [DataTestMethod] [TestCategory(TestCategory.MSSQL)] - [DataRow("/graphql/", HostMode.Development, HttpStatusCode.OK, "Banana Cake Pop", + [DataRow("/graphql/", HostMode.Development, HttpStatusCode.OK, "Nitro", DisplayName = "GraphQL endpoint with no query in development mode.")] [DataRow("/graphql", HostMode.Production, HttpStatusCode.NotFound, DisplayName = "GraphQL endpoint with no query in production mode.")] From 51b7693a064452f92f4c11551d9693782ecf2e03 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Fri, 11 Oct 2024 17:17:12 -0700 Subject: [PATCH 7/8] fix tests by ensuring auth config in stub runtimeconfig objects was defaulted properly. Also updated engine to properly set runtimeconfigprovider.IsLateConfigured == true in the event handler registration conditioned on dab not starting with a runtimeconfig. THat way in the clientroleheaderauthenticationmiddleware, we now check whether config is late bound and set SWA auth appropriately. --- src/Config/RuntimeConfigLoader.cs | 4 ++-- ...lientRoleHeaderAuthenticationMiddleware.cs | 8 +++++-- .../Configurations/RuntimeConfigProvider.cs | 15 ++++++++---- .../SimulatorIntegrationTests.cs | 4 ++-- .../Configuration/ConfigurationTests.cs | 23 +++++++++++-------- src/Service.Tests/SqlTests/SqlTestHelper.cs | 3 ++- src/Service/Startup.cs | 1 + 7 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index 0fa4df6231..cb043c1a7a 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -43,9 +43,9 @@ public RuntimeConfigLoader(HotReloadEventHandler? handler = /// Change token producer which returns an uncancelled/unsignalled change token. /// /// DabChangeToken - #pragma warning disable CA1024 // Use properties where appropriate +#pragma warning disable CA1024 // Use properties where appropriate public IChangeToken GetChangeToken() - #pragma warning restore CA1024 // Use properties where appropriate +#pragma warning restore CA1024 // Use properties where appropriate { return _changeToken; } diff --git a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs index 2955ca5a52..97bc1f5b7d 100644 --- a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs +++ b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs @@ -62,8 +62,12 @@ public async Task InvokeAsync(HttpContext httpContext) // Determine the authentication scheme to use based on dab-config.json. // Compatible with both ConfigureAuthentication and ConfigureAuthenticationV2 in startup.cs. // This means that this code is resilient to whether or not the default authentication scheme is set in startup. - AuthenticationOptions? dabAuthNOptions = _runtimeConfigProvider.GetConfig().Runtime?.Host?.Authentication; - string scheme = ResolveConfiguredAuthNScheme(dabAuthNOptions?.Provider); + string scheme = EasyAuthAuthenticationDefaults.SWAAUTHSCHEME; + if (!_runtimeConfigProvider.IsLateConfigured) + { + AuthenticationOptions? dabAuthNOptions = _runtimeConfigProvider.GetConfig().Runtime?.Host?.Authentication; + scheme = ResolveConfiguredAuthNScheme(dabAuthNOptions?.Provider); + } // authNResult will be one of: // 1. Succeeded - Authenticated diff --git a/src/Core/Configurations/RuntimeConfigProvider.cs b/src/Core/Configurations/RuntimeConfigProvider.cs index 6738ed025a..e58ac56f20 100644 --- a/src/Core/Configurations/RuntimeConfigProvider.cs +++ b/src/Core/Configurations/RuntimeConfigProvider.cs @@ -44,15 +44,12 @@ public class RuntimeConfigProvider private RuntimeConfigLoader _configLoader; private DabChangeToken _changeToken = new(); - private readonly List? _changeTokenRegistrations; + private readonly IDisposable _changeTokenRegistration; public RuntimeConfigProvider(RuntimeConfigLoader runtimeConfigLoader) { _configLoader = runtimeConfigLoader; - _changeTokenRegistrations = new List(1) - { - ChangeToken.OnChange(_configLoader.GetChangeToken, RaiseChanged) - }; + _changeTokenRegistration = ChangeToken.OnChange(_configLoader.GetChangeToken, RaiseChanged); } /// @@ -81,6 +78,14 @@ public IChangeToken GetChangeToken() return _changeToken; } + /// + /// Removes all change registration subscriptions. + /// + public void Dispose() + { + _changeTokenRegistration.Dispose(); + } + /// /// Accessor for the ConfigFilePath to avoid exposing the loader. If we are not /// loading from the file system, we return empty string. diff --git a/src/Service.Tests/Authorization/SimulatorIntegrationTests.cs b/src/Service.Tests/Authorization/SimulatorIntegrationTests.cs index a3030a2841..9baa2f2d2c 100644 --- a/src/Service.Tests/Authorization/SimulatorIntegrationTests.cs +++ b/src/Service.Tests/Authorization/SimulatorIntegrationTests.cs @@ -114,7 +114,7 @@ private static void SetupCustomRuntimeConfiguration() RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(TestHelper.GetRuntimeConfigLoader()); RuntimeConfig config = configProvider.GetConfig(); - AuthenticationOptions AuthenticationOptions = new(Provider: AuthenticationOptions.SIMULATOR_AUTHENTICATION, null); + AuthenticationOptions authenticationOptions = new(Provider: AuthenticationOptions.SIMULATOR_AUTHENTICATION, null); RuntimeConfig configWithCustomHostMode = config with { @@ -123,7 +123,7 @@ private static void SetupCustomRuntimeConfiguration() { Host = config.Runtime.Host with - { Authentication = AuthenticationOptions } + { Authentication = authenticationOptions } } }; diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 4f943417c8..12761850e4 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -942,7 +942,7 @@ public async Task TestSqlSettingPostStartupConfigurations(string configurationEn string swaTokenPayload = AuthTestHelper.CreateStaticWebAppsEasyAuthToken( addAuthenticated: true, specificRole: POST_STARTUP_CONFIG_ROLE); - message.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, swaTokenPayload); + message.Headers.Add(Config.ObjectModel.AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, swaTokenPayload); message.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, POST_STARTUP_CONFIG_ROLE); HttpResponseMessage authorizedResponse = await httpClient.SendAsync(message); Assert.AreEqual(expected: HttpStatusCode.OK, actual: authorizedResponse.StatusCode); @@ -2264,7 +2264,7 @@ public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvir string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG}" - }; + }; // Non-Hosted Scenario using (TestServer server = new(Program.CreateWebHostBuilder(args))) @@ -2896,8 +2896,8 @@ public async Task ValidateLocationHeaderWhenBaseRouteIsConfigured( const string CUSTOM_CONFIG = "custom-config.json"; - AuthenticationOptions AuthenticationOptions = new(Provider: EasyAuthType.StaticWebApps.ToString(), null); - HostOptions staticWebAppsHostOptions = new(null, AuthenticationOptions); + Config.ObjectModel.AuthenticationOptions authenticationOptions = new(Provider: EasyAuthType.StaticWebApps.ToString(), null); + HostOptions staticWebAppsHostOptions = new(null, authenticationOptions); RuntimeOptions runtimeOptions = configuration.Runtime; RuntimeOptions baseRouteEnabledRuntimeOptions = new(runtimeOptions?.Rest, runtimeOptions?.GraphQL, staticWebAppsHostOptions, "/data-api"); @@ -3223,11 +3223,11 @@ public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, Easy RuntimeConfig config = configProvider.GetConfig(); // Setup configuration - AuthenticationOptions AuthenticationOptions = new(Provider: authType.ToString(), null); + Config.ObjectModel.AuthenticationOptions authenticationOptions = new(Provider: authType.ToString(), Jwt: null); RuntimeOptions runtimeOptions = new( Rest: new(), GraphQL: new(), - Host: new(null, AuthenticationOptions, hostMode) + Host: new(Cors: null, authenticationOptions, hostMode) ); RuntimeConfig configWithCustomHostMode = config with { Runtime = runtimeOptions }; @@ -4153,6 +4153,7 @@ public async Task TestNoDepthLimitOnGrahQLInNonHostedMode(int? depthLimit) private static void CreateCustomConfigFile(bool globalRestEnabled, Dictionary entityMap) { DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); + HostOptions hostOptions = new(Cors: null, Authentication: new() { Provider = nameof(EasyAuthType.StaticWebApps) }); RuntimeConfig runtimeConfig = new( Schema: string.Empty, @@ -4160,7 +4161,7 @@ private static void CreateCustomConfigFile(bool globalRestEnabled, Dictionary()) ); diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 4f95fb0315..49a74510c3 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -317,6 +317,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC else { // Config provided during runtime. + runtimeConfigProvider.IsLateConfigured = true; runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add(async (sender, newConfig) => { isRuntimeReady = await PerformOnConfigChangeAsync(app); From caeb23a9d8f551beef83291760ab6a6cb652f006 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Mon, 14 Oct 2024 10:10:41 -0700 Subject: [PATCH 8/8] Remove commented code, update comments for ConfigureAuthenticationV2() --- .../EasyAuthAuthenticationBuilderExtensions.cs | 7 ++++--- src/Service/Startup.cs | 15 ++++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs index 32846bff51..cb1d11a149 100644 --- a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs +++ b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs @@ -24,13 +24,11 @@ public static AuthenticationBuilder AddEasyAuthAuthentication( { if (builder is null) { - throw new System.ArgumentNullException(nameof(builder)); + throw new ArgumentNullException(nameof(builder)); } if (easyAuthAuthenticationProvider is EasyAuthType.StaticWebApps) { - //builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, EasyAuthConfigureOptions>()); - builder.AddScheme( authenticationScheme: EasyAuthAuthenticationDefaults.SWAAUTHSCHEME, displayName: EasyAuthAuthenticationDefaults.SWAAUTHSCHEME, @@ -54,6 +52,7 @@ public static AuthenticationBuilder AddEasyAuthAuthentication( } /// + /// Registers the StaticWebApps and AppService EasyAuth authentication schemes. /// Used for ConfigureAuthenticationV2() where all EasyAuth schemes are registered. /// This function doesn't register EasyAuthType.AppService if the AppService environment is not detected. /// @@ -77,6 +76,8 @@ public static AuthenticationBuilder AddEnvDetectedEasyAuth(this AuthenticationBu if (appServiceEnvironmentDetected) { + // Loggers not available at this point in startup. + Console.WriteLine("AppService environment detected, configuring EasyAuth.AppService authentication scheme."); builder.AddScheme( authenticationScheme: EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME, displayName: EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME, diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 49a74510c3..4ab5f7ff96 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -175,10 +175,11 @@ public void ConfigureServices(IServiceCollection services) //Enable accessing HttpContext in RestService to get ClaimsPrincipal. services.AddHttpContextAccessor(); - // Use HotReload aware authentication configuration when in development mode - // and when runtime config was provided at startup. if (runtimeConfig is not null && runtimeConfig.Runtime?.Host?.Mode is HostMode.Development) { + // Development mode implies support for "Hot Reload". The V2 authentication function + // wires up all DAB supported authentication providers (schemes) so that at request time, + // the runtime config defined authenitication provider is used to authenticate requests. ConfigureAuthenticationV2(services, configProvider); } else @@ -536,9 +537,13 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP } /// - /// Wires up all DAB supported authentication providers. DAB uses the user configured - /// authentcation provider to determine which provider to use for authentication at request time. - /// JwtBearerOptions are hydrated when ConfigureJwtBearerOptions is called by OptionsFactory internally by .NET. + /// Registers all DAB supported authentication providers (schemes) so that at request time, + /// DAB can use the runtime config's defined provider to authenticate requests. + /// The function includes JWT specific configuration handling: + /// - IOptionsChangeTokenSource : Registers a change token source for dynamic config updates which + /// is used internally by JwtBearerHandler's OptionsMonitor to listen for changes in JwtBearerOptions. + /// - IConfigureOptions : Registers named JwtBearerOptions whose "Configure(...)" function is + /// called by OptionsFactory internally by .NET to fetch the latest configuration from the RuntimeConfigProvider. /// /// Guidance for registering IOptionsChangeTokenSource /// Guidance for registering named options.