diff --git a/src/Core/AuthenticationHelpers/AppServiceAuthenticationInformation.cs b/src/Core/AuthenticationHelpers/AppServiceAuthenticationInformation.cs index bcd836c288..0c31391148 100644 --- a/src/Core/AuthenticationHelpers/AppServiceAuthenticationInformation.cs +++ b/src/Core/AuthenticationHelpers/AppServiceAuthenticationInformation.cs @@ -15,18 +15,22 @@ public static class AppServiceAuthenticationInfo /// Environment variable key whose value represents whether AppService EasyAuth is enabled ("true" or "false"). /// public const string APPSERVICESAUTH_ENABLED_ENVVAR = "WEBSITE_AUTH_ENABLED"; - /// - /// Environment variable key whose value represents Identity Provider such as "AzureActiveDirectory" - /// - public const string APPSERVICESAUTH_IDENTITYPROVIDER_ENVVAR = "WEBSITE_AUTH_DEFAULT_PROVIDER"; + + // ── AppService messages ────────────────────────────────────────────────────────────────────── /// /// Error message used when AppService Authentication is configured in production mode in a non AppService Environment. /// - public const string APPSERVICE_PROD_MISSING_ENV_CONFIG = "AppService environment not detected while runtime is in production mode."; + public const string APPSERVICE_PROD_MISSING_ENV_CONFIG = + "AppService environment not detected while runtime is in production mode. " + + "The X-MS-CLIENT-PRINCIPAL header is not cryptographically validated by DAB and can be trivially " + + "forged when the service is not hosted behind an Azure App Service EasyAuth proxy. " + + "Set host.mode to 'development' for local testing, or deploy behind Azure App Service."; /// /// Warning message logged when AppService environment not detected (applicable to development mode). /// - public const string APPSERVICE_DEV_MISSING_ENV_CONFIG = "AppService environment not detected, EasyAuth authentication may not behave as expected."; + public const string APPSERVICE_DEV_MISSING_ENV_CONFIG = + "AppService environment not detected. The X-MS-CLIENT-PRINCIPAL header is not cryptographically " + + "validated; EasyAuth authentication may not behave as expected outside an Azure App Service environment."; /// /// Returns a best guess whether AppService is enabled in the environment by checking for @@ -39,14 +43,12 @@ public static class AppServiceAuthenticationInfo /// public static bool AreExpectedAppServiceEnvVarsPresent() { + // WEBSITE_AUTH_ENABLED is the only variable that is reliably injected by the Azure platform + // whenever App Service Authentication (EasyAuth) is enabled, regardless of how many identity + // providers are configured. WEBSITE_AUTH_DEFAULT_PROVIDER is only set when a *single* provider + // is selected; multi-provider configurations leave it unset, so it must not be required here. string? appServiceEnabled = Environment.GetEnvironmentVariable(APPSERVICESAUTH_ENABLED_ENVVAR); - string? appServiceIdentityProvider = Environment.GetEnvironmentVariable(APPSERVICESAUTH_IDENTITYPROVIDER_ENVVAR); - - if (string.IsNullOrEmpty(appServiceEnabled) || string.IsNullOrEmpty(appServiceIdentityProvider)) - { - return false; - } - - return appServiceEnabled.Equals(value: "true", comparisonType: StringComparison.OrdinalIgnoreCase); + return appServiceEnabled is not null && + appServiceEnabled.Equals(value: "true", comparisonType: StringComparison.OrdinalIgnoreCase); } } diff --git a/src/Core/AuthenticationHelpers/StaticWebAppsAuthentication.cs b/src/Core/AuthenticationHelpers/StaticWebAppsAuthentication.cs index bf123e596d..19fa7af3f3 100644 --- a/src/Core/AuthenticationHelpers/StaticWebAppsAuthentication.cs +++ b/src/Core/AuthenticationHelpers/StaticWebAppsAuthentication.cs @@ -33,6 +33,31 @@ public class StaticWebAppsClientPrincipal public IEnumerable? UserRoles { get; set; } } + /// + /// Environment variable key set by the Azure platform for every App Service and Static Web Apps + /// hosted site. Its presence is the best-effort signal that the runtime is executing behind an + /// Azure-managed proxy that injects and validates the X-MS-CLIENT-PRINCIPAL header. + /// + public const string WEBSITE_SITE_NAME_ENVVAR = "WEBSITE_SITE_NAME"; + + // ── StaticWebApps messages ─────────────────────────────────────────────────────────────────── + /// + /// Error message used when StaticWebApps Authentication is configured in production mode + /// without a detectable Azure-hosted environment. + /// + public const string SWA_PROD_MISSING_ENV_CONFIG = + "StaticWebApps environment not detected while runtime is in production mode. " + + "The X-MS-CLIENT-PRINCIPAL header is not cryptographically validated by DAB and can be trivially " + + "forged when the service is not hosted behind an Azure Static Web Apps proxy. " + + "Set host.mode to 'development' for local testing, or deploy behind Azure Static Web Apps."; + + /// + /// Warning message logged when StaticWebApps environment not detected (applicable to development mode). + /// + public const string SWA_DEV_MISSING_ENV_CONFIG = + "StaticWebApps environment not detected. The X-MS-CLIENT-PRINCIPAL header is not cryptographically " + + "validated; EasyAuth authentication may not behave as expected outside an Azure Static Web Apps environment."; + /// /// Base64 decodes and deserializes the x-ms-client-principal payload containing /// SWA token metadata. @@ -98,4 +123,14 @@ error is NotSupportedException || return identity; } + + /// + /// Returns a best-effort indication that the runtime is executing inside an Azure Static Web Apps + /// environment by checking for the presence of , which is + /// injected by the Azure platform for every SWA-hosted application. + /// + public static bool AreExpectedSWAEnvVarsPresent() + { + return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(WEBSITE_SITE_NAME_ENVVAR)); + } } diff --git a/src/Service.Tests/Caching/HealthEndpointCachingTests.cs b/src/Service.Tests/Caching/HealthEndpointCachingTests.cs index 664e6070e6..2ca6661fa4 100644 --- a/src/Service.Tests/Caching/HealthEndpointCachingTests.cs +++ b/src/Service.Tests/Caching/HealthEndpointCachingTests.cs @@ -22,6 +22,12 @@ public class HealthEndpointCachingTests { private const string CUSTOM_CONFIG_FILENAME = "custom-config.json"; + [TestInitialize] + public void SetupAuthProviderEnvironmentVariables() + { + TestHelper.SetAppServiceEnvironmentVariable(); + } + [TestCleanup] public void CleanupAfterEachTest() { diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 247685793f..8764533c14 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -735,6 +735,13 @@ public void CleanupAfterEachTest() TestHelper.UnsetAllDABEnvironmentVariables(); } + [TestInitialize] + public void SetupAuthProviderEnvironmentVariables() + { + TestHelper.SetAppServiceEnvironmentVariable(); + TestHelper.SetStaticWebAppsEnvironmentVariable(); + } + /// /// When updating config during runtime is possible, then For invalid config the Application continues to /// accept request with status code of 503. @@ -3934,23 +3941,24 @@ type Planet @model(name:""PlanetAlias"") { /// /// HostMode in Runtime config - Development or Production. /// EasyAuth auth type - AppService or StaticWebApps. - /// Whether to set the AppService host environment variables. + /// Whether to set the AppService host environment variables. + /// Whether to set the Static Web Apps host environment variable. /// Whether an error is expected. [DataTestMethod] [TestCategory(TestCategory.MSSQL)] - [DataRow(HostMode.Development, EasyAuthType.AppService, false, false, DisplayName = "AppService Dev - No EnvVars - No Error")] - [DataRow(HostMode.Development, EasyAuthType.AppService, true, false, DisplayName = "AppService Dev - EnvVars - No Error")] - [DataRow(HostMode.Production, EasyAuthType.AppService, false, false, DisplayName = "AppService Prod - No EnvVars - Error")] - [DataRow(HostMode.Production, EasyAuthType.AppService, true, false, DisplayName = "AppService Prod - EnvVars - Error")] - [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Dev - No EnvVars - No Error")] - [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Dev - EnvVars - No Error")] - [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Prod - No EnvVars - No Error")] - [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Prod - EnvVars - No Error")] - public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, EasyAuthType authType, bool setEnvVars, bool expectError) + [DataRow(HostMode.Development, EasyAuthType.AppService, false, false, false, DisplayName = "AppService Dev - No EnvVars - No Error")] + [DataRow(HostMode.Development, EasyAuthType.AppService, true, false, false, DisplayName = "AppService Dev - EnvVars - No Error")] + [DataRow(HostMode.Production, EasyAuthType.AppService, false, false, true, DisplayName = "AppService Prod - No EnvVars - Error")] + [DataRow(HostMode.Production, EasyAuthType.AppService, true, false, false, DisplayName = "AppService Prod - EnvVars - No Error")] + [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, false, false, false, DisplayName = "SWA Dev - No EnvVars - No Error")] + [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, false, true, false, DisplayName = "SWA Dev - EnvVars - No Error")] + [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, false, false, true, DisplayName = "SWA Prod - No EnvVars - Error")] + [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, false, true, false, DisplayName = "SWA Prod - EnvVars - No Error")] + public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, EasyAuthType authType, bool setAppServiceEnvVars, bool setStaticWebAppsEnvVar, bool expectError) { // Clears or sets App Service Environment Variables based on test input. - Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, setEnvVars ? "true" : null); - Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_IDENTITYPROVIDER_ENVVAR, setEnvVars ? "AzureActiveDirectory" : null); + Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, setAppServiceEnvVars ? "true" : null); + Environment.SetEnvironmentVariable(StaticWebAppsAuthentication.WEBSITE_SITE_NAME_ENVVAR, setStaticWebAppsEnvVar ? "test-site-name" : null); TestHelper.SetupDatabaseEnvironment(TestCategory.MSSQL); FileSystem fileSystem = new(); @@ -3987,7 +3995,16 @@ public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, Easy catch (DataApiBuilderException ex) { Assert.IsTrue(expectError, message: ex.Message); - Assert.AreEqual(AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG, ex.Message); + Assert.AreEqual( + expected: authType == EasyAuthType.AppService + ? AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG + : StaticWebAppsAuthentication.SWA_PROD_MISSING_ENV_CONFIG, + actual: ex.Message); + } + finally + { + Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, null); + Environment.SetEnvironmentVariable(StaticWebAppsAuthentication.WEBSITE_SITE_NAME_ENVVAR, null); } } diff --git a/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs b/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs index 99f3284a8a..227167da2b 100644 --- a/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs +++ b/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs @@ -22,6 +22,12 @@ public class HealthEndpointRolesTests private const string CUSTOM_CONFIG_FILENAME = "custom-config.json"; + [TestInitialize] + public void SetupAuthProviderEnvironmentVariables() + { + TestHelper.SetAppServiceEnvironmentVariable(); + } + [TestCleanup] public void CleanupAfterEachTest() { diff --git a/src/Service.Tests/Configuration/HealthEndpointTests.cs b/src/Service.Tests/Configuration/HealthEndpointTests.cs index b1b21c4307..0ae704a920 100644 --- a/src/Service.Tests/Configuration/HealthEndpointTests.cs +++ b/src/Service.Tests/Configuration/HealthEndpointTests.cs @@ -36,6 +36,12 @@ public class HealthEndpointTests private const string CUSTOM_CONFIG_FILENAME = "custom_config.json"; private const string BASE_DAB_URL = "http://localhost:5000"; + [TestInitialize] + public void SetupAuthProviderEnvironmentVariables() + { + TestHelper.SetAppServiceEnvironmentVariable(); + } + [TestCleanup] public void CleanupAfterEachTest() { diff --git a/src/Service.Tests/SqlTests/SqlTestHelper.cs b/src/Service.Tests/SqlTests/SqlTestHelper.cs index 32041c8fc8..5aa7cb1fe9 100644 --- a/src/Service.Tests/SqlTests/SqlTestHelper.cs +++ b/src/Service.Tests/SqlTests/SqlTestHelper.cs @@ -380,6 +380,7 @@ public static RuntimeConfig InitBasicRuntimeConfigWithNoEntity( DatabaseType dbType = DatabaseType.MSSQL, string testCategory = TestCategory.MSSQL) { + TestHelper.SetAppServiceEnvironmentVariable(); DataSource dataSource = new(dbType, GetConnectionStringFromEnvironmentConfig(environment: testCategory), new()); Config.ObjectModel.AuthenticationOptions authenticationOptions = new(Provider: nameof(EasyAuthType.AppService), null); diff --git a/src/Service.Tests/TestHelper.cs b/src/Service.Tests/TestHelper.cs index bb83f6243f..21d1c10113 100644 --- a/src/Service.Tests/TestHelper.cs +++ b/src/Service.Tests/TestHelper.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.AuthenticationHelpers; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Service.Tests.Configuration; using Humanizer; @@ -29,6 +30,18 @@ public static void UnsetAllDABEnvironmentVariables() Environment.SetEnvironmentVariable(FileSystemRuntimeConfigLoader.RUNTIME_ENVIRONMENT_VAR_NAME, null); Environment.SetEnvironmentVariable(FileSystemRuntimeConfigLoader.ASP_NET_CORE_ENVIRONMENT_VAR_NAME, null); Environment.SetEnvironmentVariable(FileSystemRuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING, null); + Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, null); + Environment.SetEnvironmentVariable(StaticWebAppsAuthentication.WEBSITE_SITE_NAME_ENVVAR, null); + } + + public static void SetAppServiceEnvironmentVariable() + { + Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, "true"); + } + + public static void SetStaticWebAppsEnvironmentVariable() + { + Environment.SetEnvironmentVariable(StaticWebAppsAuthentication.WEBSITE_SITE_NAME_ENVVAR, "dab-service-tests"); } /// diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 68364b530e..bcbaa235f8 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -1148,11 +1148,36 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP EasyAuthType easyAuthType = EnumExtensions.Deserialize(runtimeConfig.Runtime.Host.Authentication.Provider); bool isProductionMode = mode != HostMode.Development; bool appServiceEnvironmentDetected = AppServiceAuthenticationInfo.AreExpectedAppServiceEnvVarsPresent(); + bool swaEnvironmentDetected = StaticWebAppsAuthentication.AreExpectedSWAEnvVarsPresent(); if (easyAuthType == EasyAuthType.AppService && !appServiceEnvironmentDetected) { + if (isProductionMode) + { + // SECURITY: In production the X-MS-CLIENT-PRINCIPAL header is blindly trusted. + // Refuse to start when there is no evidence of an App Service EasyAuth proxy. + throw new DataApiBuilderException( + message: AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG, + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); + } + _logBuffer.BufferLog(LogLevel.Warning, AppServiceAuthenticationInfo.APPSERVICE_DEV_MISSING_ENV_CONFIG); } + else if (easyAuthType == EasyAuthType.StaticWebApps && !swaEnvironmentDetected) + { + if (isProductionMode) + { + // SECURITY: In production the X-MS-CLIENT-PRINCIPAL header is blindly trusted. + // Refuse to start when there is no evidence of a Static Web Apps proxy. + throw new DataApiBuilderException( + message: StaticWebAppsAuthentication.SWA_PROD_MISSING_ENV_CONFIG, + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); + } + + _logBuffer.BufferLog(LogLevel.Warning, StaticWebAppsAuthentication.SWA_DEV_MISSING_ENV_CONFIG); + } string defaultScheme = easyAuthType == EasyAuthType.AppService ? EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME