diff --git a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs
index fa7fdc9a25..44dac71360 100644
--- a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs
+++ b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs
@@ -181,7 +181,11 @@ public static bool IsSystemRole(string roleName)
private static string ResolveConfiguredAuthNScheme(string? configuredProviderName)
{
if (string.IsNullOrWhiteSpace(configuredProviderName)
- || string.Equals(configuredProviderName, SupportedAuthNProviders.STATIC_WEB_APPS, StringComparison.OrdinalIgnoreCase))
+ || string.Equals(configuredProviderName, SupportedAuthNProviders.UNAUTHENTICATED, StringComparison.OrdinalIgnoreCase))
+ {
+ return UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME;
+ }
+ else if (string.Equals(configuredProviderName, SupportedAuthNProviders.STATIC_WEB_APPS, StringComparison.OrdinalIgnoreCase))
{
return EasyAuthAuthenticationDefaults.SWAAUTHSCHEME;
}
@@ -193,10 +197,6 @@ private static string ResolveConfiguredAuthNScheme(string? configuredProviderNam
{
return SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME;
}
- else if (string.Equals(configuredProviderName, SupportedAuthNProviders.UNAUTHENTICATED, StringComparison.OrdinalIgnoreCase))
- {
- return UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME;
- }
else if (string.Equals(configuredProviderName, SupportedAuthNProviders.AZURE_AD, StringComparison.OrdinalIgnoreCase) ||
string.Equals(configuredProviderName, SupportedAuthNProviders.ENTRA_ID, StringComparison.OrdinalIgnoreCase))
{
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index 2c7d3ff4b0..207a374a74 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -15,6 +15,7 @@
using System.Security.Claims;
using System.Text;
using System.Text.Json;
+using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
@@ -24,6 +25,7 @@
using Azure.DataApiBuilder.Config.Telemetry;
using Azure.DataApiBuilder.Core;
using Azure.DataApiBuilder.Core.AuthenticationHelpers;
+using Azure.DataApiBuilder.Core.AuthenticationHelpers.UnauthenticatedAuthentication;
using Azure.DataApiBuilder.Core.Authorization;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
@@ -43,6 +45,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;
@@ -4108,6 +4111,181 @@ public async Task TestEngineSupportConfigWithNoAuthentication()
}
}
+ ///
+ /// Ensures a cold-started runtime with omitted authentication ignores forged EasyAuth headers,
+ /// including in development mode where all authentication handlers remain registered for hot reload.
+ ///
+ [DataTestMethod]
+ [DataRow(HostMode.Production, EasyAuthType.StaticWebApps)]
+ [DataRow(HostMode.Production, EasyAuthType.AppService)]
+ [DataRow(HostMode.Development, EasyAuthType.StaticWebApps)]
+ [DataRow(HostMode.Development, EasyAuthType.AppService)]
+ [DoNotParallelize]
+ public async Task TestColdStartOmittedAuthenticationIgnoresForgedEasyAuthHeader(HostMode hostMode, EasyAuthType payloadType)
+ {
+ TestHelper.UnsetAllDABEnvironmentVariables();
+ Assert.IsNull(Environment.GetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR));
+ Assert.IsNull(Environment.GetEnvironmentVariable(StaticWebAppsAuthentication.WEBSITE_SITE_NAME_ENVVAR));
+
+ RuntimeConfig configuration = CreateBasicRuntimeConfigWithNoEntity(
+ DatabaseType.MSSQL,
+ "Server=placeholder;");
+ RuntimeOptions runtimeOptions = configuration.Runtime! with
+ {
+ Host = new(Cors: null, Authentication: null, Mode: hostMode)
+ };
+ configuration = configuration with { Runtime = runtimeOptions };
+ JsonObject configObject = JsonNode.Parse(configuration.ToJson())!.AsObject();
+ JsonObject host = configObject["runtime"]!["host"]!.AsObject();
+ Assert.IsTrue(host.Remove("authentication"));
+ string serializedConfiguration = configObject.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
+ Assert.IsFalse(host.ContainsKey("authentication"));
+ File.WriteAllText(CUSTOM_CONFIG_FILENAME, serializedConfiguration);
+
+ string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" };
+ using TestServer server = new(Program.CreateWebHostBuilder(args));
+ Microsoft.Extensions.Hosting.IHostApplicationLifetime lifetime =
+ server.Services.GetRequiredService();
+ Assert.IsTrue(lifetime.ApplicationStarted.IsCancellationRequested, "Host did not finish starting.");
+ Assert.IsFalse(lifetime.ApplicationStopping.IsCancellationRequested, "Runtime initialization failed.");
+ RuntimeConfigProvider configProvider = server.Services.GetRequiredService();
+ Assert.IsFalse(configProvider.IsLateConfigured);
+
+ Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider =
+ server.Services.GetRequiredService();
+ Assert.IsNotNull(await schemeProvider.GetSchemeAsync(UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME));
+ Microsoft.AspNetCore.Authentication.AuthenticationScheme? defaultScheme =
+ await schemeProvider.GetDefaultAuthenticateSchemeAsync();
+ if (hostMode == HostMode.Development)
+ {
+ // With all handlers available and no default, request-time selection must ignore EasyAuth.
+ Assert.IsNull(defaultScheme);
+ Assert.IsNotNull(await schemeProvider.GetSchemeAsync(EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME));
+ Assert.IsNotNull(await schemeProvider.GetSchemeAsync(EasyAuthAuthenticationDefaults.SWAAUTHSCHEME));
+ }
+ else
+ {
+ Assert.IsNotNull(defaultScheme);
+ Assert.AreEqual(UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME, defaultScheme.Name);
+ Assert.IsNull(await schemeProvider.GetSchemeAsync(EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME));
+ Assert.IsNull(await schemeProvider.GetSchemeAsync(EasyAuthAuthenticationDefaults.SWAAUTHSCHEME));
+ }
+
+ const string FORGED_ROLE = "ForgedRole";
+ string forgedPrincipal = payloadType == EasyAuthType.StaticWebApps
+ ? AuthTestHelper.CreateStaticWebAppsEasyAuthToken(addAuthenticated: true, specificRole: FORGED_ROLE)
+ : AuthTestHelper.CreateAppServiceEasyAuthToken(
+ roleClaimType: AuthenticationOptions.ROLE_CLAIM_TYPE,
+ additionalClaims:
+ [
+ new AppServiceClaim { Typ = AuthenticationOptions.ROLE_CLAIM_TYPE, Val = FORGED_ROLE }
+ ]);
+ HttpContext context = await server.SendAsync(requestContext =>
+ {
+ requestContext.Request.Path = "/api/not-an-entity";
+ requestContext.Request.Headers[AuthenticationOptions.CLIENT_PRINCIPAL_HEADER] = forgedPrincipal;
+ requestContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = FORGED_ROLE;
+ requestContext.Request.Scheme = "https";
+ });
+
+ Assert.AreEqual(StatusCodes.Status404NotFound, context.Response.StatusCode);
+ Assert.IsNotNull(context.User.Identity);
+ Assert.IsFalse(context.User.Identity.IsAuthenticated);
+ Assert.IsFalse(context.User.IsInRole(FORGED_ROLE));
+ Assert.AreEqual(
+ AuthorizationType.Anonymous.ToString(),
+ context.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER],
+ ignoreCase: true);
+ }
+
+ ///
+ /// Preserves the existing late-configuration bootstrap and App Service request path.
+ /// Both EasyAuth handlers remain registered. Header trust in this mode is the hosting
+ /// service's responsibility; this in-process test simulates its authenticated ingress.
+ ///
+ [DataTestMethod]
+ [DataRow(CONFIGURATION_ENDPOINT)]
+ [DataRow(CONFIGURATION_ENDPOINT_V2)]
+ [DoNotParallelize]
+ public async Task TestLateConfigurationPreservesEasyAuthSchemes(string configurationEndpoint)
+ {
+ TestHelper.UnsetAllDABEnvironmentVariables();
+
+ using TestServer server = new(Program.CreateWebHostFromInMemoryUpdatableConfBuilder(Array.Empty()));
+ using HttpClient client = server.CreateClient();
+ client.BaseAddress = new Uri("https://localhost");
+ RuntimeConfigProvider configProvider = server.Services.GetRequiredService();
+ Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider =
+ server.Services.GetRequiredService();
+
+ Assert.IsTrue(configProvider.IsLateConfigured);
+ Assert.IsFalse(configProvider.TryGetLoadedConfig(out _));
+ Assert.IsNotNull(await schemeProvider.GetSchemeAsync(EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME));
+ Assert.IsNotNull(await schemeProvider.GetSchemeAsync(EasyAuthAuthenticationDefaults.SWAAUTHSCHEME));
+ Assert.IsNull(await schemeProvider.GetSchemeAsync(UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME));
+
+ Microsoft.AspNetCore.Authentication.AuthenticationScheme? defaultScheme =
+ await schemeProvider.GetDefaultAuthenticateSchemeAsync();
+ Assert.IsNotNull(defaultScheme);
+ Assert.AreEqual(EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME, defaultScheme.Name);
+
+ using HttpResponseMessage beforeHydration = await client.GetAsync("/api/not-an-entity");
+ Assert.AreEqual(HttpStatusCode.ServiceUnavailable, beforeHydration.StatusCode);
+
+ RuntimeConfig configuration = CreateBasicRuntimeConfigWithNoEntity(
+ DatabaseType.MSSQL,
+ "Server=placeholder;");
+ RuntimeOptions runtimeOptions = configuration.Runtime! with
+ {
+ Host = new(
+ Cors: null,
+ Authentication: new(Provider: EasyAuthType.AppService.ToString()),
+ Mode: HostMode.Production)
+ };
+ configuration = configuration with { Runtime = runtimeOptions };
+ using HttpRequestMessage hydrationRequest = new(HttpMethod.Post, configurationEndpoint)
+ {
+ Content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint)
+ };
+ // Honor an externally configured bootstrap token without changing process-wide state
+ // or including the token on subsequent data requests.
+ string? bootstrapToken = Environment.GetEnvironmentVariable(Startup.CONFIG_AUTH_TOKEN_ENV_VAR);
+ if (!string.IsNullOrEmpty(bootstrapToken))
+ {
+ hydrationRequest.Headers.Add(Startup.CONFIG_AUTH_HEADER, bootstrapToken);
+ }
+
+ using HttpResponseMessage hydrationResponse = await client.SendAsync(hydrationRequest);
+ Assert.AreEqual(HttpStatusCode.OK, hydrationResponse.StatusCode);
+ Assert.IsTrue(configProvider.IsLateConfigured);
+ Assert.IsTrue(configProvider.TryGetLoadedConfig(out _));
+ Assert.IsNull(Environment.GetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR));
+ Assert.IsNull(Environment.GetEnvironmentVariable(StaticWebAppsAuthentication.WEBSITE_SITE_NAME_ENVVAR));
+ Assert.IsNotNull(await schemeProvider.GetSchemeAsync(EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME));
+ Assert.IsNotNull(await schemeProvider.GetSchemeAsync(EasyAuthAuthenticationDefaults.SWAAUTHSCHEME));
+
+ const string REQUIRED_ROLE = "LateConfiguredRole";
+ string principal = AuthTestHelper.CreateAppServiceEasyAuthToken(
+ roleClaimType: AuthenticationOptions.ROLE_CLAIM_TYPE,
+ additionalClaims:
+ [
+ new AppServiceClaim { Typ = AuthenticationOptions.ROLE_CLAIM_TYPE, Val = REQUIRED_ROLE }
+ ]);
+ HttpContext context = await server.SendAsync(requestContext =>
+ {
+ requestContext.Request.Path = "/api/not-an-entity";
+ requestContext.Request.Headers[AuthenticationOptions.CLIENT_PRINCIPAL_HEADER] = principal;
+ requestContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = REQUIRED_ROLE;
+ requestContext.Request.Scheme = "https";
+ });
+
+ Assert.AreEqual(StatusCodes.Status404NotFound, context.Response.StatusCode);
+ Assert.IsNotNull(context.User.Identity);
+ Assert.IsTrue(context.User.Identity.IsAuthenticated);
+ Assert.IsTrue(context.User.IsInRole(REQUIRED_ROLE));
+ Assert.AreEqual(REQUIRED_ROLE, context.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER]);
+ }
+
///
/// In CosmosDB NoSQL, we store data in the form of JSON. Practically, JSON can be very complex.
/// But DAB doesn't support JSON with circular references e.g if 'Character.Moon' is a valid JSON Path, then
@@ -4251,8 +4429,6 @@ public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, Easy
$"--ConfigFileName={CUSTOM_CONFIG}"
};
- // When host is in Production mode with AppService as Identity Provider and the environment variables are not set
- // we do not throw an exception any longer(PR: 2943), instead log a warning to the user. In this case expectError is false.
// This test only checks for startup errors, so no requests are sent to the test server.
try
{
diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs
index b41550bf2e..5ecc77da53 100644
--- a/src/Service/Startup.cs
+++ b/src/Service/Startup.cs
@@ -1120,19 +1120,18 @@ public static ILoggerFactory CreateLoggerFactoryForHostedAndNonHostedScenario(IS
///
/// Add services necessary for Authentication Middleware and based on the loaded
/// runtime configuration set the AuthenticationOptions to be either
- /// EasyAuth based (by default) or JwtBearerOptions.
- /// When no runtime configuration is set on engine startup, set the
- /// default authentication scheme to EasyAuth.
+ /// Unauthenticated (by default), EasyAuth based, or JwtBearerOptions.
+ /// When no runtime configuration is available on engine startup, set the
+ /// default authentication scheme to EasyAuth for late configuration.
///
/// The service collection where authentication services are added.
/// The provider used to load runtime configuration.
private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigProvider runtimeConfigurationProvider)
{
- if (runtimeConfigurationProvider.TryGetConfig(out RuntimeConfig? runtimeConfig) &&
- runtimeConfig.Runtime?.Host?.Authentication is not null)
+ if (runtimeConfigurationProvider.TryGetConfig(out RuntimeConfig? runtimeConfig))
{
- AuthenticationOptions authOptions = runtimeConfig.Runtime.Host.Authentication;
- HostMode mode = runtimeConfig.Runtime.Host.Mode;
+ AuthenticationOptions authOptions = runtimeConfig.Runtime?.Host?.Authentication ?? new();
+ HostMode mode = runtimeConfig.Runtime?.Host?.Mode ?? HostMode.Production;
if (authOptions.IsJwtConfiguredIdentityProvider())
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
@@ -1151,7 +1150,7 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP
}
else if (authOptions.IsEasyAuthAuthenticationProvider())
{
- EasyAuthType easyAuthType = EnumExtensions.Deserialize(runtimeConfig.Runtime.Host.Authentication.Provider);
+ EasyAuthType easyAuthType = EnumExtensions.Deserialize(authOptions.Provider);
bool isProductionMode = mode != HostMode.Development;
bool appServiceEnvironmentDetected = AppServiceAuthenticationInfo.AreExpectedAppServiceEnvVarsPresent();
bool swaEnvironmentDetected = StaticWebAppsAuthentication.AreExpectedSWAEnvVarsPresent();