Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@ public static class AppServiceAuthenticationInfo
/// 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";

// ── AppService messages ──────────────────────────────────────────────────────────────────────
/// <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.";
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.";
/// <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.";
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.";

/// <summary>
/// Returns a best guess whether AppService is enabled in the environment by checking for
Expand All @@ -39,14 +43,12 @@ public static class AppServiceAuthenticationInfo
/// </summary>
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);
}
}
35 changes: 35 additions & 0 deletions src/Core/AuthenticationHelpers/StaticWebAppsAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,31 @@ public class StaticWebAppsClientPrincipal
public IEnumerable<string>? UserRoles { get; set; }
}

/// <summary>
/// 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.
/// </summary>
Comment thread
Aniruddh25 marked this conversation as resolved.
public const string WEBSITE_SITE_NAME_ENVVAR = "WEBSITE_SITE_NAME";

// ── StaticWebApps messages ───────────────────────────────────────────────────────────────────
/// <summary>
/// Error message used when StaticWebApps Authentication is configured in production mode
/// without a detectable Azure-hosted environment.
/// </summary>
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.";

/// <summary>
/// Warning message logged when StaticWebApps environment not detected (applicable to development mode).
/// </summary>
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.";

/// <summary>
/// Base64 decodes and deserializes the x-ms-client-principal payload containing
/// SWA token metadata.
Expand Down Expand Up @@ -98,4 +123,14 @@ error is NotSupportedException ||

return identity;
}

/// <summary>
/// Returns a best-effort indication that the runtime is executing inside an Azure Static Web Apps
/// environment by checking for the presence of <see cref="WEBSITE_SITE_NAME_ENVVAR"/>, which is
/// injected by the Azure platform for every SWA-hosted application.
/// </summary>
Comment thread
Aniruddh25 marked this conversation as resolved.
public static bool AreExpectedSWAEnvVarsPresent()
{
return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(WEBSITE_SITE_NAME_ENVVAR));
}
}
6 changes: 6 additions & 0 deletions src/Service.Tests/Caching/HealthEndpointCachingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
43 changes: 30 additions & 13 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,13 @@ public void CleanupAfterEachTest()
TestHelper.UnsetAllDABEnvironmentVariables();
}

[TestInitialize]
public void SetupAuthProviderEnvironmentVariables()
{
TestHelper.SetAppServiceEnvironmentVariable();
TestHelper.SetStaticWebAppsEnvironmentVariable();
}

/// <summary>
/// When updating config during runtime is possible, then For invalid config the Application continues to
/// accept request with status code of 503.
Expand Down Expand Up @@ -3934,23 +3941,24 @@ type Planet @model(name:""PlanetAlias"") {
/// </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="setAppServiceEnvVars">Whether to set the AppService host environment variables.</param>
/// <param name="setStaticWebAppsEnvVar">Whether to set the Static Web Apps host environment variable.</param>
/// <param name="expectError">Whether an error is expected.</param>
[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();
Expand Down Expand Up @@ -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);
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/Service.Tests/Configuration/HealthEndpointRolesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
6 changes: 6 additions & 0 deletions src/Service.Tests/Configuration/HealthEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
1 change: 1 addition & 0 deletions src/Service.Tests/SqlTests/SqlTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
13 changes: 13 additions & 0 deletions src/Service.Tests/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");
}

/// <summary>
Expand Down
25 changes: 25 additions & 0 deletions src/Service/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1148,11 +1148,36 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP
EasyAuthType easyAuthType = EnumExtensions.Deserialize<EasyAuthType>(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);
Comment thread
Aniruddh25 marked this conversation as resolved.
}

_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
Expand Down
Loading