diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs index e0089eacfa..8ee064e262 100644 --- a/src/Cli.Tests/ConfigureOptionsTests.cs +++ b/src/Cli.Tests/ConfigureOptionsTests.cs @@ -512,6 +512,7 @@ public void TestUpdateCorsAllowCredentialsHostSettings(bool allowCredentialsValu [DataRow("staticWebApps", DisplayName = "Update authentication.provider to StaticWebApps for Host.")] [DataRow("Appservice", DisplayName = "Update authentication.provider to AppService for Host.")] [DataRow("azuread", DisplayName = "Update authentication.provider to AzureAD for Host.")] + [DataRow("entraid", DisplayName = "Update authentication.provider to EntraID for Host.")] public void TestUpdateAuthenticationProviderHostSettings(string authenticationProviderValue) { // Arrange -> all the setup which includes creating options. diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index c1dfcbf9ce..28fecfb2d5 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -459,11 +459,13 @@ public void TestUpdateCacheTtlRuntimeSettings(string ttl, bool isSuccess) /// neither EasyAuth or Simulator as Authentication provider. /// It checks correct generation of config with provider, audience and issuer. /// - [TestMethod] - public void TestVerifyAuthenticationOptions() + [DataTestMethod] + [DataRow("AzureAD")] + [DataRow("EntraID")] + public void TestVerifyAuthenticationOptions(string authenticationProvider) { string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--database-type", "mssql", - "--auth.provider", "AzureAD", "--auth.audience", "aud-xxx", "--auth.issuer", "issuer-xxx" }; + "--auth.provider", authenticationProvider, "--auth.audience", "aud-xxx", "--auth.issuer", "issuer-xxx" }; Program.Execute(initArgs, _cliLogger!, _fileSystem!, _runtimeConfigLoader!); Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig)); @@ -471,7 +473,7 @@ public void TestVerifyAuthenticationOptions() Assert.IsNotNull(runtimeConfig.Runtime); Assert.IsNotNull(runtimeConfig.Runtime.Host); - Assert.AreEqual("AzureAD", runtimeConfig.Runtime.Host.Authentication?.Provider); + Assert.AreEqual(authenticationProvider, runtimeConfig.Runtime.Host.Authentication?.Provider); Assert.AreEqual("aud-xxx", runtimeConfig.Runtime.Host.Authentication?.Jwt?.Audience); Assert.AreEqual("issuer-xxx", runtimeConfig.Runtime.Host.Authentication?.Jwt?.Issuer); } @@ -1114,6 +1116,7 @@ public async Task TestExitOfRuntimeEngineWithInvalidConfig( [DataRow("StaticWebApps", false)] [DataRow("AppService", true)] [DataRow("AzureAD", true)] + [DataRow("EntraID", true)] public void TestBaseRouteIsConfigurableForSWA(string authProvider, bool isExceptionExpected) { string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--host-mode", "development", "--database-type", "mssql", diff --git a/src/Cli.Tests/InitTests.cs b/src/Cli.Tests/InitTests.cs index a7f6116adf..80eda44788 100644 --- a/src/Cli.Tests/InitTests.cs +++ b/src/Cli.Tests/InitTests.cs @@ -302,6 +302,7 @@ public void EnsureFailureOnReInitializingExistingConfig() [DataRow("AppService", null, null, DisplayName = "AppService with no audience and no issuer specified.")] [DataRow("Simulator", null, null, DisplayName = "Simulator with no audience and no issuer specified.")] [DataRow("AzureAD", "aud-xxx", "issuer-xxx", DisplayName = "AzureAD with both audience and issuer specified.")] + [DataRow("EntraID", "aud-xxx", "issuer-xxx", DisplayName = "EntraID with both audience and issuer specified.")] public Task EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders( string authenticationProvider, string? audience, @@ -426,7 +427,7 @@ public Task GraphQLPathWithoutStartingSlashWillHaveItAdded() /// /// b. When --graphql.multiple-create.enabled option is not used /// - In this case, fields related to multiple mutation and multiple create operations will NOT be written to the config file. - /// + /// /// [DataTestMethod] [DataRow(DatabaseType.MSSQL, CliBool.True, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for MsSQL database type")] @@ -453,7 +454,7 @@ public Task VerifyCorrectConfigGenerationWithMultipleMutationOptions(DatabaseTyp if (databaseType is DatabaseType.CosmosDB_NoSQL) { - // A schema file is added since its mandatory for CosmosDB_NoSQL + // A schema file is added since its mandatory for CosmosDB_NoSQL ((MockFileSystem)_fileSystem!).AddFile(TEST_SCHEMA_FILE, new MockFileData("")); options = new( diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt new file mode 100644 index 0000000000..7a67eca701 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt @@ -0,0 +1,34 @@ +{ + DataSource: { + DatabaseType: MSSQL, + Options: { + set-session-context: false + } + }, + Runtime: { + Rest: { + Enabled: true, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: EntraID, + Jwt: { + Audience: aud-xxx, + Issuer: issuer-xxx + } + }, + Mode: Production + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/TestHelper.cs b/src/Cli.Tests/TestHelper.cs index 6e06abf61b..a5a0079797 100644 --- a/src/Cli.Tests/TestHelper.cs +++ b/src/Cli.Tests/TestHelper.cs @@ -175,6 +175,32 @@ public static Process ExecuteDabCommand(string command, string flags) } }"; + /// + /// Only Runtime section containing both rest and graphql enabled. The authentication provider can be replaced with <>. + /// + public const string RUNTIME_SECTION_JWT_AUTHENTICATION_PLACEHOLDER = @" + ""runtime"": { + ""rest"": { + ""path"": ""/api"", + ""enabled"": true + }, + ""graphql"": { + ""path"": ""/graphql"", + ""enabled"": true, + ""allow-introspection"": true + }, + ""host"": { + ""mode"": ""development"", + ""cors"": { + ""origins"": [], + ""allow-credentials"": false + }, + ""authentication"": { + ""provider"": ""<>"" + } + } + }"; + /// /// Configuration with unresolved environment variable references on /// properties of various data types (string, enum, bool, int). diff --git a/src/Cli.Tests/UtilsTests.cs b/src/Cli.Tests/UtilsTests.cs index 84c36a54f7..486d09f253 100644 --- a/src/Cli.Tests/UtilsTests.cs +++ b/src/Cli.Tests/UtilsTests.cs @@ -207,6 +207,10 @@ public void TestApiPathIsWellFormed(string apiPath, bool expectSuccess) [DataRow("AzureAD", null, "issuer-xxx", false, DisplayName = "FAIL: AzureAD incorrectly configured with no audience specified.")] [DataRow("AzureAD", "aud-xxx", null, false, DisplayName = "FAIL: AzureAD incorrectly configured with no issuer specified.")] [DataRow("AzureAD", null, null, false, DisplayName = "FAIL: AzureAD incorrectly configured with no audience or issuer specified.")] + [DataRow("EntraID", "aud-xxx", "issuer-xxx", true, DisplayName = "PASS: EntraID correctly configured with both audience and issuer.")] + [DataRow("EntraID", null, "issuer-xxx", false, DisplayName = "FAIL: EntraID incorrectly configured with no audience specified.")] + [DataRow("EntraID", "aud-xxx", null, false, DisplayName = "FAIL: EntraID incorrectly configured with no issuer specified.")] + [DataRow("EntraID", null, null, false, DisplayName = "FAIL: EntraID incorrectly configured with no audience or issuer specified.")] public void TestValidateAudienceAndIssuerForAuthenticationProvider( string authenticationProvider, string? audience, diff --git a/src/Cli.Tests/ValidateConfigTests.cs b/src/Cli.Tests/ValidateConfigTests.cs index 9560d5ce7e..318819f8be 100644 --- a/src/Cli.Tests/ValidateConfigTests.cs +++ b/src/Cli.Tests/ValidateConfigTests.cs @@ -145,6 +145,32 @@ public void TestValidateConfigFailsWithInvalidGraphQLDepthLimit(object? depthLim } } + /// + /// This Test is used to verify that DAB fails when the JWT properties are missing for OAuth based providers + /// + [DataTestMethod] + [DataRow("AzureAD")] + [DataRow("EntraID")] + public void TestMissingJwtProperties(string authScheme) + { + string ConfigWithJwtAuthentication = $"{{{SAMPLE_SCHEMA_DATA_SOURCE}, {RUNTIME_SECTION_JWT_AUTHENTICATION_PLACEHOLDER}, \"entities\": {{ }}}}"; + ConfigWithJwtAuthentication = ConfigWithJwtAuthentication.Replace("<>", authScheme, StringComparison.OrdinalIgnoreCase); + + // create an empty config file + ((MockFileSystem)_fileSystem!).AddFile(TEST_RUNTIME_CONFIG_FILE, ConfigWithJwtAuthentication); + + ValidateOptions validateOptions = new(TEST_RUNTIME_CONFIG_FILE); + + try + { + Assert.IsFalse(ConfigGenerator.IsConfigValid(validateOptions, _runtimeConfigLoader!, _fileSystem!)); + } + catch (Exception ex) + { + Assert.Fail($"Unexpected Exception thrown: {ex.Message}"); + } + } + /// /// This Test is used to verify that the validate command is able to catch when data source field or entities field is missing. /// diff --git a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs index fa0af6d645..16ca00fb72 100644 --- a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs +++ b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs @@ -92,7 +92,7 @@ protected override Task HandleAuthenticateAsync() if (claimsPrincipal is not null) { // AuthenticationTicket is Asp.Net Core Abstraction of Authentication information - // Ref: aspnetcore/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs + // Ref: aspnetcore/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs AuthenticationTicket ticket = new(claimsPrincipal, EasyAuthAuthenticationDefaults.AUTHENTICATIONSCHEME); AuthenticateResult success = AuthenticateResult.Success(ticket); return Task.FromResult(success); diff --git a/src/Core/AuthenticationHelpers/SupportedAuthNProviders.cs b/src/Core/AuthenticationHelpers/SupportedAuthNProviders.cs index e10a1e3977..70a6809074 100644 --- a/src/Core/AuthenticationHelpers/SupportedAuthNProviders.cs +++ b/src/Core/AuthenticationHelpers/SupportedAuthNProviders.cs @@ -6,9 +6,12 @@ namespace Azure.DataApiBuilder.Core.AuthenticationHelpers; internal static class SupportedAuthNProviders { public const string APP_SERVICE = "AppService"; + public const string AZURE_AD = "AzureAD"; public const string ENTRA_ID = "EntraID"; + public const string GENERIC_OAUTH = "Custom"; public const string SIMULATOR = "Simulator"; + public const string STATIC_WEB_APPS = "StaticWebApps"; } diff --git a/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs b/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs index 15db8198b8..8b01d29961 100644 --- a/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs +++ b/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs @@ -59,14 +59,16 @@ public void ValidateEasyAuthConfig() } } - [TestMethod("AuthN validation passes when all values are provided when provider not EasyAuth")] - public void ValidateJwtConfigParamsSet() + [DataTestMethod("AuthN validation passes when all values are provided when provider not EasyAuth")] + [DataRow("AzureAD")] + [DataRow("EntraID")] + public void ValidateJwtConfigParamsSet(string authenticationProvider) { JwtOptions jwt = new( Audience: "12345", Issuer: "https://login.microsoftonline.com/common"); AuthenticationOptions authNConfig = new( - Provider: "AzureAD", + Provider: authenticationProvider, Jwt: jwt); RuntimeConfig config = CreateRuntimeConfigWithOptionalAuthN(authNConfig); @@ -108,14 +110,16 @@ public void ValidateAuthNSectionNotNecessary() } } - [TestMethod("AuthN validation fails when either Issuer or Audience not provided not EasyAuth")] - public void ValidateFailureWithIncompleteJwtConfig() + [DataTestMethod("AuthN validation fails when either Issuer or Audience not provided not EasyAuth")] + [DataRow("AzureAD")] + [DataRow("EntraID")] + public void ValidateFailureWithIncompleteJwtConfig(string authenticationProvider) { JwtOptions jwt = new( Audience: "12345", Issuer: string.Empty); AuthenticationOptions authNConfig = new( - Provider: "AzureAD", + Provider: authenticationProvider, Jwt: jwt); RuntimeConfig config = CreateRuntimeConfigWithOptionalAuthN(authNConfig); @@ -136,7 +140,7 @@ public void ValidateFailureWithIncompleteJwtConfig() Audience: string.Empty, Issuer: DEFAULT_ISSUER); authNConfig = new( - Provider: "AzureAD", + Provider: authenticationProvider, Jwt: jwt); config = CreateRuntimeConfigWithOptionalAuthN(authNConfig); diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 2ad87c71f2..c94d5a9a80 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -1402,7 +1402,7 @@ public async Task TestValidateConfigForValidDepthLimit(int? depthLimit) /// /// This method validates that depth-limit outside the valid range should fail validation - /// during `dab validate` and `dab start`. + /// during `dab validate` and `dab start`. /// /// /// @@ -4396,7 +4396,7 @@ public async Task ValidateNextLinkUsage() /// /// Tests the enforcement of depth limit restrictions on GraphQL queries and mutations in non-hosted mode. - /// Verifies that requests exceeding the specified depth limit result in a BadRequest, + /// Verifies that requests exceeding the specified depth limit result in a BadRequest, /// while requests within the limit succeed with the expected status code. /// Also verifies that the error message contains the current and allowed max depth limit value. /// Example: