diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json
index b684cc28ac..b441b57303 100644
--- a/schemas/dab.draft.schema.json
+++ b/schemas/dab.draft.schema.json
@@ -368,6 +368,10 @@
{
"const": "Custom",
"description": "Custom authentication provider defined by the user. Use the JWT property to configure the custom provider."
+ },
+ {
+ "const": "Unauthenticated",
+ "description": "Unauthenticated provider where all operations run as anonymous. Use when Data API builder is behind an app gateway or APIM where authentication is handled externally."
}
],
"default": "AppService"
diff --git a/src/Cli.Tests/InitTests.cs b/src/Cli.Tests/InitTests.cs
index 051bfdf7a7..96ba1ad66b 100644
--- a/src/Cli.Tests/InitTests.cs
+++ b/src/Cli.Tests/InitTests.cs
@@ -301,6 +301,7 @@ public void EnsureFailureOnReInitializingExistingConfig()
[DataRow("StaticWebApps", null, null, DisplayName = "StaticWebApps with no audience and no issuer specified.")]
[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("Unauthenticated", null, null, DisplayName = "Unauthenticated 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(
diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_47836da0dfbdc458.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_47836da0dfbdc458.verified.txt
new file mode 100644
index 0000000000..55843cf207
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_47836da0dfbdc458.verified.txt
@@ -0,0 +1,50 @@
+{
+ DataSource: {
+ DatabaseType: MSSQL,
+ Options: {
+ set-session-context: false
+ }
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Mcp: {
+ Enabled: true,
+ Path: /mcp,
+ DmlTools: {
+ AllToolsEnabled: true,
+ DescribeEntities: true,
+ CreateRecord: true,
+ ReadRecords: true,
+ UpdateRecord: true,
+ DeleteRecord: true,
+ ExecuteEntity: true,
+ UserProvidedAllTools: false,
+ UserProvidedDescribeEntities: false,
+ UserProvidedCreateRecord: false,
+ UserProvidedReadRecords: false,
+ UserProvidedUpdateRecord: false,
+ UserProvidedDeleteRecord: false,
+ UserProvidedExecuteEntity: false
+ }
+ },
+ Host: {
+ Cors: {
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: Unauthenticated
+ },
+ Mode: Production
+ }
+ },
+ Entities: []
+}
diff --git a/src/Cli.Tests/ValidateConfigTests.cs b/src/Cli.Tests/ValidateConfigTests.cs
index e40a32e291..03cac6de82 100644
--- a/src/Cli.Tests/ValidateConfigTests.cs
+++ b/src/Cli.Tests/ValidateConfigTests.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
using Serilog;
@@ -359,4 +360,58 @@ private async Task ValidatePropertyOptionsFails(ConfigureOptions options)
JsonSchemaValidationResult result = await validator.ValidateConfigSchema(config, TEST_RUNTIME_CONFIG_FILE, mockLoggerFactory.Object);
Assert.IsFalse(result.IsValid);
}
+
+ ///
+ /// Test that the Unauthenticated provider is correctly identified by the IsUnauthenticatedAuthenticationProvider method.
+ ///
+ [TestMethod]
+ public void TestIsUnauthenticatedAuthenticationProviderMethod()
+ {
+ // Test with Unauthenticated provider
+ AuthenticationOptions unauthenticatedOptions = new(Provider: "Unauthenticated");
+ Assert.IsTrue(unauthenticatedOptions.IsUnauthenticatedAuthenticationProvider());
+
+ // Test case-insensitivity
+ AuthenticationOptions unauthenticatedOptionsLower = new(Provider: "unauthenticated");
+ Assert.IsTrue(unauthenticatedOptionsLower.IsUnauthenticatedAuthenticationProvider());
+
+ // Test that other providers are not identified as Unauthenticated
+ AuthenticationOptions appServiceOptions = new(Provider: "AppService");
+ Assert.IsFalse(appServiceOptions.IsUnauthenticatedAuthenticationProvider());
+
+ AuthenticationOptions simulatorOptions = new(Provider: "Simulator");
+ Assert.IsFalse(simulatorOptions.IsUnauthenticatedAuthenticationProvider());
+ }
+
+ ///
+ /// Test that Unauthenticated provider does not require JWT configuration.
+ ///
+ [TestMethod]
+ public void TestUnauthenticatedProviderDoesNotRequireJwt()
+ {
+ AuthenticationOptions unauthenticatedOptions = new(Provider: "Unauthenticated");
+ Assert.IsFalse(unauthenticatedOptions.IsJwtConfiguredIdentityProvider());
+ }
+
+ ///
+ /// Test that entities with non-anonymous roles are correctly identified when
+ /// Unauthenticated provider is configured. This validates the core detection logic
+ /// used by IsConfigValid to emit warnings.
+ ///
+ [DataTestMethod]
+ [DataRow("authenticated", true, DisplayName = "Authenticated role should be flagged as non-anonymous")]
+ [DataRow("customRole", true, DisplayName = "Custom role should be flagged as non-anonymous")]
+ [DataRow("anonymous", false, DisplayName = "Anonymous role should not be flagged")]
+ [DataRow("Anonymous", false, DisplayName = "Anonymous role (case-insensitive) should not be flagged")]
+ public void TestUnauthenticatedProviderNonAnonymousRoleDetection(string role, bool shouldWarn)
+ {
+ // Arrange: Create an entity permission with the specified role
+ EntityPermission permission = new(Role: role, Actions: new EntityAction[] { new(Action: EntityActionOperation.Read, Fields: null, Policy: null) });
+
+ // Act: Check if the role is non-anonymous (the logic used in IsConfigValid)
+ bool isNonAnonymous = !permission.Role.Equals("anonymous", StringComparison.OrdinalIgnoreCase);
+
+ // Assert: Verify the detection logic works correctly
+ Assert.AreEqual(shouldWarn, isNonAnonymous, $"Role '{role}' detection mismatch");
+ }
}
diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index 648edc1950..ec9b0cd3d3 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -2444,6 +2444,22 @@ public static bool IsConfigValid(ValidateOptions options, FileSystemRuntimeConfi
}
}
}
+
+ // Warn if Unauthenticated provider is used with authenticated or custom roles
+ if (config.Runtime?.Host?.Authentication?.IsUnauthenticatedAuthenticationProvider() == true)
+ {
+ foreach (KeyValuePair entity in config.Entities.Where(e => e.Value.Permissions is not null))
+ {
+ foreach (EntityPermission permission in entity.Value.Permissions!.Where(p => !p.Role.Equals("anonymous", StringComparison.OrdinalIgnoreCase)))
+ {
+ _logger.LogWarning(
+ "Entity '{EntityName}' has permission configured for role '{Role}' but authentication provider is 'Unauthenticated'. " +
+ "All requests will be treated as anonymous.",
+ entity.Key,
+ permission.Role);
+ }
+ }
+ }
}
}
diff --git a/src/Cli/Utils.cs b/src/Cli/Utils.cs
index 48edd4411c..c1ff7f2a99 100644
--- a/src/Cli/Utils.cs
+++ b/src/Cli/Utils.cs
@@ -516,11 +516,12 @@ public static bool ValidateAudienceAndIssuerForJwtProvider(
string? issuer)
{
if (Enum.TryParse(authenticationProvider, ignoreCase: true, out _)
- || AuthenticationOptions.SIMULATOR_AUTHENTICATION == authenticationProvider)
+ || AuthenticationOptions.SIMULATOR_AUTHENTICATION.Equals(authenticationProvider, StringComparison.OrdinalIgnoreCase)
+ || AuthenticationOptions.UNAUTHENTICATED_AUTHENTICATION.Equals(authenticationProvider, StringComparison.OrdinalIgnoreCase))
{
if (!(string.IsNullOrWhiteSpace(audience)) || !(string.IsNullOrWhiteSpace(issuer)))
{
- _logger.LogWarning("Audience and Issuer can't be set for EasyAuth or Simulator authentication.");
+ _logger.LogWarning("Audience and Issuer can't be set for EasyAuth, Simulator, or Unauthenticated authentication.");
return true;
}
}
@@ -528,7 +529,7 @@ public static bool ValidateAudienceAndIssuerForJwtProvider(
{
if (string.IsNullOrWhiteSpace(audience) || string.IsNullOrWhiteSpace(issuer))
{
- _logger.LogError($"Authentication providers other than EasyAuth and Simulator require both Audience and Issuer.");
+ _logger.LogError($"Authentication providers other than EasyAuth, Simulator, and Unauthenticated require both Audience and Issuer.");
return false;
}
}
diff --git a/src/Config/ObjectModel/AuthenticationOptions.cs b/src/Config/ObjectModel/AuthenticationOptions.cs
index a937168493..036f55876f 100644
--- a/src/Config/ObjectModel/AuthenticationOptions.cs
+++ b/src/Config/ObjectModel/AuthenticationOptions.cs
@@ -32,9 +32,17 @@ public record AuthenticationOptions(string Provider = nameof(EasyAuthType.AppSer
/// True when development mode should authenticate all requests.
public bool IsAuthenticationSimulatorEnabled() => Provider.Equals(SIMULATOR_AUTHENTICATION, StringComparison.OrdinalIgnoreCase);
+ public const string UNAUTHENTICATED_AUTHENTICATION = "Unauthenticated";
+
+ ///
+ /// Returns whether the configured Provider value matches the unauthenticated authentication type.
+ ///
+ /// True when all operations run as anonymous.
+ public bool IsUnauthenticatedAuthenticationProvider() => Provider.Equals(UNAUTHENTICATED_AUTHENTICATION, StringComparison.OrdinalIgnoreCase);
+
///
/// A shorthand method to determine whether JWT is configured for the current authentication provider.
///
/// True if the provider is enabled for JWT, otherwise false.
- public bool IsJwtConfiguredIdentityProvider() => !IsEasyAuthAuthenticationProvider() && !IsAuthenticationSimulatorEnabled();
+ public bool IsJwtConfiguredIdentityProvider() => !IsEasyAuthAuthenticationProvider() && !IsAuthenticationSimulatorEnabled() && !IsUnauthenticatedAuthenticationProvider();
};
diff --git a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs
index c83de9ed3a..513e81a137 100644
--- a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs
+++ b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs
@@ -4,6 +4,7 @@
using System.Security.Claims;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator;
+using Azure.DataApiBuilder.Core.AuthenticationHelpers.UnauthenticatedAuthenticationHandler;
using Azure.DataApiBuilder.Core.Authorization;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
@@ -192,6 +193,10 @@ private static string ResolveConfiguredAuthNScheme(string? configuredProviderNam
{
return SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME;
}
+ else if (string.Equals(configuredProviderName, SupportedAuthNProviders.UNAUTHENTICATED, StringComparison.OrdinalIgnoreCase))
+ {
+ return UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME;
+ }
else if (string.Equals(configuredProviderName, SupportedAuthNProviders.AZURE_AD, StringComparison.OrdinalIgnoreCase) ||
string.Equals(configuredProviderName, SupportedAuthNProviders.ENTRA_ID, StringComparison.OrdinalIgnoreCase))
{
diff --git a/src/Core/AuthenticationHelpers/SupportedAuthNProviders.cs b/src/Core/AuthenticationHelpers/SupportedAuthNProviders.cs
index 70a6809074..cc543ee28e 100644
--- a/src/Core/AuthenticationHelpers/SupportedAuthNProviders.cs
+++ b/src/Core/AuthenticationHelpers/SupportedAuthNProviders.cs
@@ -14,4 +14,6 @@ internal static class SupportedAuthNProviders
public const string SIMULATOR = "Simulator";
public const string STATIC_WEB_APPS = "StaticWebApps";
+
+ public const string UNAUTHENTICATED = "Unauthenticated";
}
diff --git a/src/Core/AuthenticationHelpers/UnauthenticatedAuthenticationHandler/UnauthenticatedAuthenticationBuilderExtensions.cs b/src/Core/AuthenticationHelpers/UnauthenticatedAuthenticationHandler/UnauthenticatedAuthenticationBuilderExtensions.cs
new file mode 100644
index 0000000000..6cf6b79e94
--- /dev/null
+++ b/src/Core/AuthenticationHelpers/UnauthenticatedAuthenticationHandler/UnauthenticatedAuthenticationBuilderExtensions.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.AspNetCore.Authentication;
+
+namespace Azure.DataApiBuilder.Core.AuthenticationHelpers.UnauthenticatedAuthenticationHandler;
+
+///
+/// Extension methods related to Unauthenticated authentication.
+/// This class allows setting up Unauthenticated authentication in the startup class with
+/// a single call to .AddAuthentication(scheme).AddUnauthenticatedAuthentication()
+///
+public static class UnauthenticatedAuthenticationBuilderExtensions
+{
+ ///
+ /// Add authentication with Unauthenticated provider.
+ ///
+ /// Authentication builder.
+ /// The builder, to chain commands.
+ public static AuthenticationBuilder AddUnauthenticatedAuthentication(this AuthenticationBuilder builder)
+ {
+ if (builder is null)
+ {
+ throw new System.ArgumentNullException(nameof(builder));
+ }
+
+ builder.AddScheme(
+ authenticationScheme: UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME,
+ displayName: UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME,
+ configureOptions: null);
+
+ return builder;
+ }
+}
diff --git a/src/Core/AuthenticationHelpers/UnauthenticatedAuthenticationHandler/UnauthenticatedAuthenticationDefaults.cs b/src/Core/AuthenticationHelpers/UnauthenticatedAuthenticationHandler/UnauthenticatedAuthenticationDefaults.cs
new file mode 100644
index 0000000000..03f8222ecd
--- /dev/null
+++ b/src/Core/AuthenticationHelpers/UnauthenticatedAuthenticationHandler/UnauthenticatedAuthenticationDefaults.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.DataApiBuilder.Core.AuthenticationHelpers.UnauthenticatedAuthenticationHandler;
+
+///
+/// Default values related to UnauthenticatedAuthentication handler.
+///
+public static class UnauthenticatedAuthenticationDefaults
+{
+ ///
+ /// The default value used for UnauthenticatedAuthenticationOptions.AuthenticationScheme.
+ ///
+ public const string AUTHENTICATIONSCHEME = "UnauthenticatedAuthentication";
+}
diff --git a/src/Core/AuthenticationHelpers/UnauthenticatedAuthenticationHandler/UnauthenticatedAuthenticationHandler.cs b/src/Core/AuthenticationHelpers/UnauthenticatedAuthenticationHandler/UnauthenticatedAuthenticationHandler.cs
new file mode 100644
index 0000000000..552e44b228
--- /dev/null
+++ b/src/Core/AuthenticationHelpers/UnauthenticatedAuthenticationHandler/UnauthenticatedAuthenticationHandler.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Azure.DataApiBuilder.Core.AuthenticationHelpers.UnauthenticatedAuthenticationHandler;
+
+///
+/// This class is used to best integrate with ASP.NET Core AuthenticationHandler base class.
+/// When "Unauthenticated" is configured, this handler authenticates the user as anonymous,
+/// without reading any HTTP authentication headers.
+///
+public class UnauthenticatedAuthenticationHandler : AuthenticationHandler
+{
+ ///
+ /// Constructor for the UnauthenticatedAuthenticationHandler.
+ /// Note the parameters are required by the base class.
+ ///
+ /// Authentication options.
+ /// Logger factory.
+ /// URL encoder.
+ public UnauthenticatedAuthenticationHandler(
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder)
+ : base(options, logger, encoder)
+ {
+ }
+
+ ///
+ /// Returns an unauthenticated ClaimsPrincipal for all requests.
+ /// The ClaimsPrincipal has no identity and no claims, representing an anonymous user.
+ ///
+ /// An authentication result to ASP.NET Core library authentication mechanisms
+ protected override Task HandleAuthenticateAsync()
+ {
+ // ClaimsIdentity without authenticationType means the user is not authenticated (anonymous)
+ ClaimsIdentity identity = new();
+ ClaimsPrincipal claimsPrincipal = new(identity);
+
+ AuthenticationTicket ticket = new(claimsPrincipal, UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME);
+ AuthenticateResult success = AuthenticateResult.Success(ticket);
+ return Task.FromResult(success);
+ }
+}
diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs
index 333bf57234..e61154fc90 100644
--- a/src/Service/Startup.cs
+++ b/src/Service/Startup.cs
@@ -13,6 +13,7 @@
using Azure.DataApiBuilder.Config.Utilities;
using Azure.DataApiBuilder.Core.AuthenticationHelpers;
using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator;
+using Azure.DataApiBuilder.Core.AuthenticationHelpers.UnauthenticatedAuthenticationHandler;
using Azure.DataApiBuilder.Core.Authorization;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
@@ -772,7 +773,7 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP
{
AuthenticationOptions authOptions = runtimeConfig.Runtime.Host.Authentication;
HostMode mode = runtimeConfig.Runtime.Host.Mode;
- if (!authOptions.IsAuthenticationSimulatorEnabled() && !authOptions.IsEasyAuthAuthenticationProvider())
+ if (!authOptions.IsAuthenticationSimulatorEnabled() && !authOptions.IsEasyAuthAuthenticationProvider() && !authOptions.IsUnauthenticatedAuthenticationProvider())
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
@@ -809,6 +810,11 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP
_logger.LogInformation("Registered EasyAuth scheme: {Scheme}", defaultScheme);
}
+ else if (authOptions.IsUnauthenticatedAuthenticationProvider())
+ {
+ services.AddAuthentication(UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME)
+ .AddUnauthenticatedAuthentication();
+ }
else if (mode == HostMode.Development && authOptions.IsAuthenticationSimulatorEnabled())
{
services.AddAuthentication(SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME)
@@ -850,7 +856,8 @@ private static void ConfigureAuthenticationV2(IServiceCollection services, Runti
services.AddAuthentication()
.AddEnvDetectedEasyAuth()
.AddJwtBearer()
- .AddSimulatorAuthentication();
+ .AddSimulatorAuthentication()
+ .AddUnauthenticatedAuthentication();
}
///