diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs
index b368227a75..a1355679fb 100644
--- a/src/Cli.Tests/ConfigureOptionsTests.cs
+++ b/src/Cli.Tests/ConfigureOptionsTests.cs
@@ -1094,5 +1094,130 @@ private void SetupFileSystemWithInitialConfig(string jsonConfig)
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? config));
Assert.IsNotNull(config.Runtime);
}
+
+ ///
+ /// Tests adding user-delegated-auth configuration options individually or together.
+ /// Verifies that enabled and database-audience properties can be set independently or combined.
+ /// Also verifies default values for properties not explicitly set.
+ /// Commands:
+ /// - dab configure --data-source.user-delegated-auth.enabled true
+ /// - dab configure --data-source.user-delegated-auth.database-audience "https://database.windows.net"
+ /// - dab configure --data-source.user-delegated-auth.enabled true --data-source.user-delegated-auth.database-audience "https://database.windows.net"
+ ///
+ [DataTestMethod]
+ [DataRow(true, null, DisplayName = "Set enabled=true only")]
+ [DataRow(null, "https://database.windows.net", DisplayName = "Set database-audience only")]
+ [DataRow(true, "https://database.windows.net", DisplayName = "Set both enabled and database-audience")]
+ public void TestAddUserDelegatedAuthConfiguration(bool? enabledValue, string? audienceValue)
+ {
+ // Arrange
+ SetupFileSystemWithInitialConfig(INITIAL_CONFIG);
+
+ ConfigureOptions options = new(
+ dataSourceUserDelegatedAuthEnabled: enabledValue,
+ dataSourceUserDelegatedAuthDatabaseAudience: audienceValue,
+ config: TEST_RUNTIME_CONFIG_FILE
+ );
+
+ // Act
+ bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!);
+
+ // Assert
+ Assert.IsTrue(isSuccess);
+ string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE);
+ Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config));
+ Assert.IsNotNull(config.DataSource);
+ Assert.IsNotNull(config.DataSource.UserDelegatedAuth);
+
+ // Verify enabled value (if set, use provided value; otherwise defaults to false)
+ if (enabledValue.HasValue)
+ {
+ Assert.AreEqual(enabledValue.Value, config.DataSource.UserDelegatedAuth.Enabled);
+ }
+ else
+ {
+ Assert.IsFalse(config.DataSource.UserDelegatedAuth.Enabled);
+ }
+
+ // Verify database-audience value
+ if (audienceValue is not null)
+ {
+ Assert.AreEqual(audienceValue, config.DataSource.UserDelegatedAuth.DatabaseAudience);
+ }
+ else
+ {
+ Assert.IsNull(config.DataSource.UserDelegatedAuth.DatabaseAudience);
+ }
+
+ // Verify provider is set to default
+ Assert.AreEqual("EntraId", config.DataSource.UserDelegatedAuth.Provider);
+ }
+
+ ///
+ /// Tests that enabling user-delegated-auth on a non-MSSQL database fails.
+ /// This method verifies that user-delegated-auth is only allowed for MSSQL database type.
+ /// Command: dab configure --data-source.database-type postgresql --data-source.user-delegated-auth.enabled true
+ ///
+ [DataTestMethod]
+ [DataRow("postgresql", DisplayName = "Fail when enabling user-delegated-auth on PostgreSQL")]
+ [DataRow("mysql", DisplayName = "Fail when enabling user-delegated-auth on MySQL")]
+ [DataRow("cosmosdb_nosql", DisplayName = "Fail when enabling user-delegated-auth on CosmosDB")]
+ public void TestFailureWhenEnablingUserDelegatedAuthOnNonMSSQLDatabase(string dbType)
+ {
+ // Arrange
+ SetupFileSystemWithInitialConfig(INITIAL_CONFIG);
+
+ ConfigureOptions options = new(
+ dataSourceDatabaseType: dbType,
+ dataSourceUserDelegatedAuthEnabled: true,
+ config: TEST_RUNTIME_CONFIG_FILE
+ );
+
+ // Act
+ bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!);
+
+ // Assert
+ Assert.IsFalse(isSuccess);
+ }
+
+ ///
+ /// Tests updating existing user-delegated-auth configuration by changing the database-audience.
+ /// Verifies that the database-audience can be updated while preserving the enabled setting.
+ /// Also validates JSON structure: verifies user-delegated-auth is correctly nested under data-source
+ /// with proper JSON property names (enabled, provider, database-audience).
+ ///
+ [TestMethod]
+ public void TestUpdateUserDelegatedAuthDatabaseAudience()
+ {
+ // Arrange - Config with existing user-delegated-auth section
+ SetupFileSystemWithInitialConfig(TestHelper.CONFIG_WITH_USER_DELEGATED_AUTH);
+
+ string newAudience = "https://database.usgovcloudapi.net";
+ ConfigureOptions options = new(
+ dataSourceUserDelegatedAuthDatabaseAudience: newAudience,
+ config: TEST_RUNTIME_CONFIG_FILE
+ );
+
+ // Act
+ bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!);
+
+ // Assert
+ Assert.IsTrue(isSuccess);
+ string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE);
+ Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config));
+ Assert.IsNotNull(config.DataSource);
+ Assert.IsNotNull(config.DataSource.UserDelegatedAuth);
+ Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled);
+ Assert.AreEqual(newAudience, config.DataSource.UserDelegatedAuth.DatabaseAudience);
+ Assert.AreEqual("EntraId", config.DataSource.UserDelegatedAuth.Provider);
+
+ // Verify JSON structure using JObject to ensure correct nesting
+ JObject configJson = JObject.Parse(updatedConfig);
+ JToken? userDelegatedAuthSection = configJson["data-source"]?["user-delegated-auth"];
+ Assert.IsNotNull(userDelegatedAuthSection);
+ Assert.AreEqual(newAudience, (string?)userDelegatedAuthSection["database-audience"]);
+ Assert.AreEqual(true, (bool?)userDelegatedAuthSection["enabled"]);
+ Assert.AreEqual("EntraId", (string?)userDelegatedAuthSection["provider"]);
+ }
}
}
diff --git a/src/Cli.Tests/TestHelper.cs b/src/Cli.Tests/TestHelper.cs
index 4c461fc5ba..a75e359ee4 100644
--- a/src/Cli.Tests/TestHelper.cs
+++ b/src/Cli.Tests/TestHelper.cs
@@ -279,6 +279,46 @@ public static Process ExecuteDabCommand(string command, string flags)
public const string CONFIG_WITH_DISABLED_GLOBAL_REST_GRAPHQL = $"{{{SAMPLE_SCHEMA_DATA_SOURCE},{RUNTIME_SECTION_WITH_DISABLED_REST_GRAPHQL}}}";
+ ///
+ /// A config json with user-delegated-auth enabled. This is used in tests to verify updating existing
+ /// user-delegated-auth configuration.
+ ///
+ public const string CONFIG_WITH_USER_DELEGATED_AUTH = @"
+ {
+ ""$schema"": """ + DAB_DRAFT_SCHEMA_TEST_PATH + @""",
+ ""data-source"": {
+ ""database-type"": ""mssql"",
+ ""connection-string"": """ + SAMPLE_TEST_CONN_STRING + @""",
+ ""user-delegated-auth"": {
+ ""enabled"": true,
+ ""provider"": ""EntraId"",
+ ""database-audience"": ""https://database.windows.net""
+ }
+ },
+ ""runtime"": {
+ ""rest"": {
+ ""enabled"": true,
+ ""path"": ""/api""
+ },
+ ""graphql"": {
+ ""enabled"": true,
+ ""path"": ""/graphql"",
+ ""allow-introspection"": true
+ },
+ ""host"": {
+ ""mode"": ""development"",
+ ""cors"": {
+ ""origins"": [],
+ ""allow-credentials"": false
+ },
+ ""authentication"": {
+ ""provider"": ""StaticWebApps""
+ }
+ }
+ },
+ ""entities"": {}
+ }";
+
public const string SINGLE_ENTITY = @"
{
""entities"": {
diff --git a/src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs b/src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs
new file mode 100644
index 0000000000..29110a5a7c
--- /dev/null
+++ b/src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Cli.Tests
+{
+ [TestClass]
+ public class UserDelegatedAuthRuntimeParsingTests
+ {
+ [TestMethod]
+ public void TestRuntimeCanParseUserDelegatedAuthConfig()
+ {
+ // Arrange
+ string configJson = @"{
+ ""$schema"": ""test"",
+ ""data-source"": {
+ ""database-type"": ""mssql"",
+ ""connection-string"": ""testconnectionstring"",
+ ""user-delegated-auth"": {
+ ""enabled"": true,
+ ""database-audience"": ""https://database.windows.net""
+ }
+ },
+ ""runtime"": {
+ ""rest"": {
+ ""enabled"": true,
+ ""path"": ""/api""
+ },
+ ""graphql"": {
+ ""enabled"": true,
+ ""path"": ""/graphql"",
+ ""allow-introspection"": true
+ },
+ ""host"": {
+ ""mode"": ""development"",
+ ""cors"": {
+ ""origins"": [],
+ ""allow-credentials"": false
+ },
+ ""authentication"": {
+ ""provider"": ""StaticWebApps""
+ }
+ }
+ },
+ ""entities"": {}
+ }";
+
+ // Act
+ bool success = RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig? config);
+
+ // Assert
+ Assert.IsTrue(success);
+ Assert.IsNotNull(config);
+ Assert.IsNotNull(config.DataSource.UserDelegatedAuth);
+ Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled);
+ Assert.AreEqual("https://database.windows.net", config.DataSource.UserDelegatedAuth.DatabaseAudience);
+ }
+
+ [TestMethod]
+ public void TestRuntimeCanParseConfigWithoutUserDelegatedAuth()
+ {
+ // Arrange
+ string configJson = @"{
+ ""$schema"": ""test"",
+ ""data-source"": {
+ ""database-type"": ""mssql"",
+ ""connection-string"": ""testconnectionstring""
+ },
+ ""runtime"": {
+ ""rest"": {
+ ""enabled"": true,
+ ""path"": ""/api""
+ },
+ ""graphql"": {
+ ""enabled"": true,
+ ""path"": ""/graphql"",
+ ""allow-introspection"": true
+ },
+ ""host"": {
+ ""mode"": ""development"",
+ ""cors"": {
+ ""origins"": [],
+ ""allow-credentials"": false
+ },
+ ""authentication"": {
+ ""provider"": ""StaticWebApps""
+ }
+ }
+ },
+ ""entities"": {}
+ }";
+
+ // Act
+ bool success = RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig? config);
+
+ // Assert
+ Assert.IsTrue(success);
+ Assert.IsNotNull(config);
+ Assert.IsNull(config.DataSource.UserDelegatedAuth);
+ }
+ }
+}
diff --git a/src/Cli/Commands/ConfigureOptions.cs b/src/Cli/Commands/ConfigureOptions.cs
index 14234d24d7..262cbc9145 100644
--- a/src/Cli/Commands/ConfigureOptions.cs
+++ b/src/Cli/Commands/ConfigureOptions.cs
@@ -29,6 +29,8 @@ public ConfigureOptions(
string? dataSourceOptionsSchema = null,
bool? dataSourceOptionsSetSessionContext = null,
string? dataSourceHealthName = null,
+ bool? dataSourceUserDelegatedAuthEnabled = null,
+ string? dataSourceUserDelegatedAuthDatabaseAudience = null,
int? depthLimit = null,
bool? runtimeGraphQLEnabled = null,
string? runtimeGraphQLPath = null,
@@ -84,6 +86,8 @@ public ConfigureOptions(
DataSourceOptionsSchema = dataSourceOptionsSchema;
DataSourceOptionsSetSessionContext = dataSourceOptionsSetSessionContext;
DataSourceHealthName = dataSourceHealthName;
+ DataSourceUserDelegatedAuthEnabled = dataSourceUserDelegatedAuthEnabled;
+ DataSourceUserDelegatedAuthDatabaseAudience = dataSourceUserDelegatedAuthDatabaseAudience;
// GraphQL
DepthLimit = depthLimit;
RuntimeGraphQLEnabled = runtimeGraphQLEnabled;
@@ -160,6 +164,12 @@ public ConfigureOptions(
[Option("data-source.health.name", Required = false, HelpText = "Identifier for data source in health check report.")]
public string? DataSourceHealthName { get; }
+ [Option("data-source.user-delegated-auth.enabled", Required = false, HelpText = "Enable user-delegated authentication (OBO) for Azure SQL and SQL Server. Default: false (boolean).")]
+ public bool? DataSourceUserDelegatedAuthEnabled { get; }
+
+ [Option("data-source.user-delegated-auth.database-audience", Required = false, HelpText = "Database resource identifier for token acquisition (e.g., https://database.windows.net for Azure SQL).")]
+ public string? DataSourceUserDelegatedAuthDatabaseAudience { get; }
+
[Option("runtime.graphql.depth-limit", Required = false, HelpText = "Max allowed depth of the nested query. Allowed values: (0,2147483647] inclusive. Default is infinity. Use -1 to remove limit.")]
public int? DepthLimit { get; }
diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index 9a3401a55a..6c51f002b7 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -643,6 +643,7 @@ private static bool TryUpdateConfiguredDataSourceOptions(
DatabaseType dbType = runtimeConfig.DataSource.DatabaseType;
string dataSourceConnectionString = runtimeConfig.DataSource.ConnectionString;
DatasourceHealthCheckConfig? datasourceHealthCheckConfig = runtimeConfig.DataSource.Health;
+ UserDelegatedAuthOptions? userDelegatedAuthConfig = runtimeConfig.DataSource.UserDelegatedAuth;
if (options.DataSourceDatabaseType is not null)
{
@@ -714,8 +715,41 @@ private static bool TryUpdateConfiguredDataSourceOptions(
}
}
+ // Handle user-delegated-auth options
+ if (options.DataSourceUserDelegatedAuthEnabled is not null
+ || options.DataSourceUserDelegatedAuthDatabaseAudience is not null)
+ {
+ // Determine the enabled state: use new value if provided, otherwise preserve existing
+ bool enabled = options.DataSourceUserDelegatedAuthEnabled
+ ?? userDelegatedAuthConfig?.Enabled
+ ?? false;
+
+ // Validate that user-delegated-auth is only used with MSSQL when enabled=true
+ if (enabled && !DatabaseType.MSSQL.Equals(dbType))
+ {
+ _logger.LogError("user-delegated-auth is only supported for database-type 'mssql'.");
+ return false;
+ }
+
+ // Get database-audience: use new value if provided, otherwise preserve existing
+ string? databaseAudience = options.DataSourceUserDelegatedAuthDatabaseAudience
+ ?? userDelegatedAuthConfig?.DatabaseAudience;
+
+ // Get provider: preserve existing or use default "EntraId"
+ string? provider = userDelegatedAuthConfig?.Provider ?? "EntraId";
+
+ // Create or update user-delegated-auth config
+ userDelegatedAuthConfig = new UserDelegatedAuthOptions(
+ Enabled: enabled,
+ Provider: provider,
+ DatabaseAudience: databaseAudience);
+ }
+
dbOptions = EnumerableUtilities.IsNullOrEmpty(dbOptions) ? null : dbOptions;
- DataSource dataSource = new(dbType, dataSourceConnectionString, dbOptions, datasourceHealthCheckConfig);
+ DataSource dataSource = new(dbType, dataSourceConnectionString, dbOptions, datasourceHealthCheckConfig)
+ {
+ UserDelegatedAuth = userDelegatedAuthConfig
+ };
runtimeConfig = runtimeConfig with { DataSource = dataSource };
return runtimeConfig != null;