Skip to content
59 changes: 59 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -810,6 +811,64 @@ public async Task TestPathRewriteMiddlewareForGraphQL(
}
}

/// <summary>
/// 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.
/// </summary>
/// <param name="hostMode">HostMode in Runtime config - Development or Production.</param>
/// <param name="authType">EasyAuth auth type - AppService or StaticWebApps.</param>
/// <param name="setEnvVars">Whether to set the AppService host environment variables.</param>
/// <param name="expectError">Whether an error is expected.</param>
[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<GlobalSettingsType, object> 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);
}
}

/// <summary>
/// Integration test that validates schema introspection requests fail
/// when allow-introspection is false in the runtime configuration.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;

namespace Azure.DataApiBuilder.Service.AuthenticationHelpers
{
/// <summary>
/// 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.
/// </summary>
/// <seealso cref="https://github.com/AzureAD/microsoft-identity-web/blob/master/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationInformation.cs"/>
public static class AppServiceAuthenticationInfo
{
/// <summary>
/// Environment variable key whose value represents whether AppService EasyAuth is enabled ("true" or "false").
/// </summary>
public const string APPSERVICESAUTH_ENABLED_ENVVAR = "WEBSITE_AUTH_ENABLED";
/// <summary>
/// Environment variable key whose value represents Identity Provider such as "AzureActiveDirectory"
/// </summary>
public const string APPSERVICESAUTH_IDENTITYPROVIDER_ENVVAR = "WEBSITE_AUTH_DEFAULT_PROVIDER";
/// <summary>
/// Error message used when AppService Authentication is configured in production mode in a non AppService Environment.
/// </summary>
public const string APPSERVICE_PROD_MISSING_ENV_CONFIG = "AppService environment not detected while runtime is in production mode.";
/// <summary>
/// Warning message logged when AppService environment not detected (applicable to development mode).
/// </summary>
public const string APPSERVICE_DEV_MISSING_ENV_CONFIG = "AppService environment not detected, EasyAuth authentication may not behave as expected.";

/// <summary>
/// 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.
/// </summary>
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
namespace Azure.DataApiBuilder.Service.AuthenticationHelpers
{
/// <summary>
/// Default values related to StaticWebAppAuthentication handler.
/// Default values related to EasyAuthAuthentication handler.
/// </summary>
public static class EasyAuthAuthenticationDefaults
{
/// <summary>
/// The default value used for StaticWebAppAuthenticationOptions.AuthenticationScheme.
/// The default value used for EasyAuthAuthenticationOptions.AuthenticationScheme.
/// </summary>
public const string AUTHENTICATIONSCHEME = "EasyAuthAuthentication";

Expand Down
26 changes: 21 additions & 5 deletions src/Service/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC
/// </summary>
/// <param name="services">The service collection where authentication services are added.</param>
/// <param name="runtimeConfigurationProvider">The provider used to load runtime configuration.</param>
private static void ConfigureAuthentication(IServiceCollection services, RuntimeConfigProvider runtimeConfigurationProvider)
private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigProvider runtimeConfigurationProvider)
{
if (runtimeConfigurationProvider.TryGetRuntimeConfiguration(out RuntimeConfig? runtimeConfig) && runtimeConfig.AuthNConfig != null)
{
Expand All @@ -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())
{
Expand Down