diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index d1ecd6d0f6..5692311c73 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Service.AuthenticationHelpers; using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Configurations; using Azure.DataApiBuilder.Service.Controllers; @@ -810,6 +811,64 @@ public async Task TestPathRewriteMiddlewareForGraphQL( } } + /// + /// Tests that Startup.cs properly handles EasyAuth authentication configuration. + /// AppService as Identity Provider while in Production mode will result in startup error. + /// An Azure AppService environment has environment variables on the host which indicate + /// the environment is, in fact, an AppService environment. + /// + /// HostMode in Runtime config - Development or Production. + /// EasyAuth auth type - AppService or StaticWebApps. + /// Whether to set the AppService host environment variables. + /// Whether an error is expected. + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow(HostModeType.Development, EasyAuthType.AppService, false, false, DisplayName = "AppService Dev - No EnvVars - No Error")] + [DataRow(HostModeType.Development, EasyAuthType.AppService, true, false, DisplayName = "AppService Dev - EnvVars - No Error")] + [DataRow(HostModeType.Production, EasyAuthType.AppService, false, true, DisplayName = "AppService Prod - No EnvVars - Error")] + [DataRow(HostModeType.Production, EasyAuthType.AppService, true, false, DisplayName = "AppService Prod - EnvVars - Error")] + [DataRow(HostModeType.Development, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Dev - No EnvVars - No Error")] + [DataRow(HostModeType.Development, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Dev - EnvVars - No Error")] + [DataRow(HostModeType.Production, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Prod - No EnvVars - No Error")] + [DataRow(HostModeType.Production, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Prod - EnvVars - No Error")] + public void TestProductionModeAppServiceEnvironmentCheck(HostModeType hostMode, EasyAuthType authType, bool setEnvVars, 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); + + RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(MSSQL_ENVIRONMENT); + RuntimeConfig config = configProvider.GetRuntimeConfiguration(); + + // Setup configuration + AuthenticationConfig authenticationConfig = new(Provider: authType.ToString()); + HostGlobalSettings customHostGlobalSettings = config.HostGlobalSettings with { Mode = hostMode, Authentication = authenticationConfig }; + JsonElement serializedCustomHostGlobalSettings = JsonSerializer.SerializeToElement(customHostGlobalSettings, RuntimeConfig.SerializerOptions); + Dictionary customRuntimeSettings = new(config.RuntimeSettings); + customRuntimeSettings.Remove(GlobalSettingsType.Host); + customRuntimeSettings.Add(GlobalSettingsType.Host, serializedCustomHostGlobalSettings); + RuntimeConfig configWithCustomHostMode = config with { RuntimeSettings = customRuntimeSettings }; + + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText(path: CUSTOM_CONFIG, contents: JsonSerializer.Serialize(configWithCustomHostMode, RuntimeConfig.SerializerOptions)); + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG}" + }; + + // This test only checks for startup errors, so no requests are sent to the test server. + try + { + using TestServer server = new(Program.CreateWebHostBuilder(args)); + Assert.IsFalse(expectError, message: "Expected error faulting AppService config in production mode."); + } + catch (DataApiBuilderException ex) + { + Assert.IsTrue(expectError, message: ex.Message); + Assert.AreEqual(AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG, ex.Message); + } + } + /// /// Integration test that validates schema introspection requests fail /// when allow-introspection is false in the runtime configuration. diff --git a/src/Service/AuthenticationHelpers/AppServiceAuthenticationInformation.cs b/src/Service/AuthenticationHelpers/AppServiceAuthenticationInformation.cs new file mode 100644 index 0000000000..eb4675a91c --- /dev/null +++ b/src/Service/AuthenticationHelpers/AppServiceAuthenticationInformation.cs @@ -0,0 +1,52 @@ +using System; + +namespace Azure.DataApiBuilder.Service.AuthenticationHelpers +{ + /// + /// Info about the App Services configuration on the host. This class is an abridged mirror of + /// Microsoft.Identity.Web's AppServicesAuthenticationInformation.cs helper class used to + /// detect whether the app is running in an Azure App Service environment. + /// + /// + 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"; + /// + /// 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."; + /// + /// 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."; + + /// + /// Returns a best guess whether AppService is enabled in the environment by checking for + /// existence and value population of known AppService environment variables. + /// This check is determined to be "best guess" because environment variables could be + /// manually added or overridden. + /// This check's purpose is to help warn developers that an AppService environment is not detected + /// where the DataApiBuilder service is executing and DataApiBuilder is configured to use AppService + /// as the identity provider. + /// + public static bool AreExpectedAppServiceEnvVarsPresent() + { + 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); + } + } +} diff --git a/src/Service/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs b/src/Service/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs index acf036e248..4d6cc322ae 100644 --- a/src/Service/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs +++ b/src/Service/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs @@ -1,12 +1,12 @@ namespace Azure.DataApiBuilder.Service.AuthenticationHelpers { /// - /// Default values related to StaticWebAppAuthentication handler. + /// Default values related to EasyAuthAuthentication handler. /// public static class EasyAuthAuthenticationDefaults { /// - /// The default value used for StaticWebAppAuthenticationOptions.AuthenticationScheme. + /// The default value used for EasyAuthAuthenticationOptions.AuthenticationScheme. /// public const string AUTHENTICATIONSCHEME = "EasyAuthAuthentication"; diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 5ea8f012f3..272c06c67d 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -383,7 +383,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC /// /// The service collection where authentication services are added. /// The provider used to load runtime configuration. - private static void ConfigureAuthentication(IServiceCollection services, RuntimeConfigProvider runtimeConfigurationProvider) + private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigProvider runtimeConfigurationProvider) { if (runtimeConfigurationProvider.TryGetRuntimeConfiguration(out RuntimeConfig? runtimeConfig) && runtimeConfig.AuthNConfig != null) { @@ -404,11 +404,27 @@ private static void ConfigureAuthentication(IServiceCollection services, Runtime } else if (runtimeConfig.IsEasyAuthAuthenticationProvider()) { + EasyAuthType easyAuthType = (EasyAuthType)Enum.Parse(typeof(EasyAuthType), runtimeConfig.AuthNConfig.Provider, ignoreCase: true); + bool isProductionMode = !runtimeConfigurationProvider.IsDeveloperMode(); + bool appServiceEnvironmentDetected = AppServiceAuthenticationInfo.AreExpectedAppServiceEnvVarsPresent(); + + if (easyAuthType == EasyAuthType.AppService && !appServiceEnvironmentDetected) + { + if (isProductionMode) + { + throw new DataApiBuilderException( + message: AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG, + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + else + { + _logger.LogWarning(AppServiceAuthenticationInfo.APPSERVICE_DEV_MISSING_ENV_CONFIG); + } + } + services.AddAuthentication(EasyAuthAuthenticationDefaults.AUTHENTICATIONSCHEME) - .AddEasyAuthAuthentication( - (EasyAuthType)Enum.Parse(typeof(EasyAuthType), - runtimeConfig.AuthNConfig.Provider, - ignoreCase: true)); + .AddEasyAuthAuthentication(easyAuthAuthenticationProvider: easyAuthType); } else if (runtimeConfigurationProvider.IsDeveloperMode() && runtimeConfig.IsAuthenticationSimulatorEnabled()) {