Skip to content
4 changes: 4 additions & 0 deletions schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/Cli.Tests/ConfigureOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ public void TestUpdateCorsAllowCredentialsHostSettings(bool allowCredentialsValu
[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.")]
[DataRow("Unauthenticated", DisplayName = "Update authentication.provider to Unauthenticated for Host.")]
public void TestUpdateAuthenticationProviderHostSettings(string authenticationProviderValue)
{
// Arrange -> all the setup which includes creating options.
Expand Down
5 changes: 4 additions & 1 deletion src/Cli.Tests/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1121,12 +1121,15 @@ public async Task TestExitOfRuntimeEngineWithInvalidConfig(
[DataRow("AppService", true)]
[DataRow("AzureAD", true)]
[DataRow("EntraID", true)]
[DataRow("Unauthenticated", true)]
public void TestBaseRouteIsConfigurableForSWA(string authProvider, bool isExceptionExpected)
{
string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--host-mode", "development", "--database-type", "mssql",
"--connection-string", SAMPLE_TEST_CONN_STRING, "--auth.provider", authProvider, "--runtime.base-route", "base-route" };

if (!Enum.TryParse(authProvider, ignoreCase: true, out EasyAuthType _))
if (!Enum.TryParse(authProvider, ignoreCase: true, out EasyAuthType _) &&
!authProvider.Equals("Unauthenticated", StringComparison.OrdinalIgnoreCase) &&
!authProvider.Equals("Simulator", StringComparison.OrdinalIgnoreCase))
{
string[] audIssuers = { "--auth.audience", "aud-xxx", "--auth.issuer", "issuer-xxx" };
initArgs = initArgs.Concat(audIssuers).ToArray();
Expand Down
1 change: 1 addition & 0 deletions src/Cli.Tests/InitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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: []
}
4 changes: 4 additions & 0 deletions src/Cli.Tests/UtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ public void TestApiPathIsWellFormed(string apiPath, bool expectSuccess)
[DataRow("Simulator", null, "issuer-xxx", true, DisplayName = "PASS: Issuer ignored with Simulator.")]
[DataRow("Simulator", "aud-xxx", null, true, DisplayName = "PASS: Audience ignored with Simulator.")]
[DataRow("Simulator", null, null, true, DisplayName = "PASS: Simulator correctly configured with neither audience nor issuer.")]
[DataRow("Unauthenticated", "aud-xxx", "issuer-xxx", true, DisplayName = "PASS: Audience and Issuer ignored with Unauthenticated.")]
[DataRow("Unauthenticated", null, "issuer-xxx", true, DisplayName = "PASS: Issuer ignored with Unauthenticated.")]
[DataRow("Unauthenticated", "aud-xxx", null, true, DisplayName = "PASS: Audience ignored with Unauthenticated.")]
[DataRow("Unauthenticated", null, null, true, DisplayName = "PASS: Unauthenticated correctly configured with neither audience nor issuer.")]
[DataRow("AzureAD", "aud-xxx", "issuer-xxx", true, DisplayName = "PASS: AzureAD correctly configured with both audience and issuer.")]
[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.")]
Expand Down
1 change: 1 addition & 0 deletions src/Cli.Tests/ValidateConfigTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
bool hasNonAnonymousRoles = config.Entities
.Where(e => e.Value.Permissions is not null)
.SelectMany(e => e.Value.Permissions!)
.Any(p => !p.Role.Equals("anonymous", StringComparison.OrdinalIgnoreCase));

if (hasNonAnonymousRoles)
{
_logger.LogWarning(
"Authentication provider is 'Unauthenticated' but some entities have permissions configured for non-anonymous roles. " +
"All requests will be treated as anonymous.");
}
}
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/Cli/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -516,19 +516,20 @@ public static bool ValidateAudienceAndIssuerForJwtProvider(
string? issuer)
{
if (Enum.TryParse<EasyAuthType>(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;
}
}
else
{
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;
}
}
Expand Down
10 changes: 9 additions & 1 deletion src/Config/ObjectModel/AuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ public record AuthenticationOptions(string Provider = nameof(EasyAuthType.AppSer
/// <returns>True when development mode should authenticate all requests.</returns>
public bool IsAuthenticationSimulatorEnabled() => Provider.Equals(SIMULATOR_AUTHENTICATION, StringComparison.OrdinalIgnoreCase);

public const string UNAUTHENTICATED_AUTHENTICATION = "Unauthenticated";

/// <summary>
/// Returns whether the configured Provider value matches the Unauthenticated authentication type.
/// </summary>
/// <returns>True if Provider is Unauthenticated type.</returns>
public bool IsUnauthenticatedAuthenticationProvider() => Provider.Equals(UNAUTHENTICATED_AUTHENTICATION, StringComparison.OrdinalIgnoreCase);

/// <summary>
/// A shorthand method to determine whether JWT is configured for the current authentication provider.
/// </summary>
/// <returns>True if the provider is enabled for JWT, otherwise false.</returns>
public bool IsJwtConfiguredIdentityProvider() => !IsEasyAuthAuthenticationProvider() && !IsAuthenticationSimulatorEnabled();
public bool IsJwtConfiguredIdentityProvider() => !IsEasyAuthAuthenticationProvider() && !IsAuthenticationSimulatorEnabled() && !IsUnauthenticatedAuthenticationProvider();
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Security.Claims;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator;
using Azure.DataApiBuilder.Core.AuthenticationHelpers.UnauthenticatedAuthentication;
using Azure.DataApiBuilder.Core.Authorization;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
Expand Down Expand Up @@ -192,6 +193,10 @@ private static string ResolveConfiguredAuthNScheme(string? configuredProviderNam
{
return SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME;
}
else if (string.Equals(configuredProviderName, SupportedAuthNProviders.UNAUTHENTICATED, StringComparison.OrdinalIgnoreCase))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if this need more testing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test case ValidateUnauthenticatedProviderIdentification in AuthenticationConfigValidatorUnitTests.cs to validate the Unauthenticated provider detection. Additional coverage is provided through the existing tests that now include Unauthenticated: TestValidateAudienceAndIssuerForAuthenticationProvider, TestBaseRouteIsConfigurableForSWA, and TestUpdateAuthenticationProviderHostSettings.

{
return UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME;
}
else if (string.Equals(configuredProviderName, SupportedAuthNProviders.AZURE_AD, StringComparison.OrdinalIgnoreCase) ||
string.Equals(configuredProviderName, SupportedAuthNProviders.ENTRA_ID, StringComparison.OrdinalIgnoreCase))
{
Expand Down
2 changes: 2 additions & 0 deletions src/Core/AuthenticationHelpers/SupportedAuthNProviders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Authentication;

namespace Azure.DataApiBuilder.Core.AuthenticationHelpers.UnauthenticatedAuthentication;

/// <summary>
/// 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()
/// </summary>
public static class UnauthenticatedAuthenticationBuilderExtensions
{
/// <summary>
/// Add authentication with Unauthenticated provider.
/// </summary>
/// <param name="builder">Authentication builder.</param>
/// <returns>The builder, to chain commands.</returns>
public static AuthenticationBuilder AddUnauthenticatedAuthentication(this AuthenticationBuilder builder)
{
if (builder is null)
{
throw new System.ArgumentNullException(nameof(builder));
}

builder.AddScheme<AuthenticationSchemeOptions, UnauthenticatedAuthenticationHandler>(
authenticationScheme: UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME,
displayName: UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME,
configureOptions: null);

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.DataApiBuilder.Core.AuthenticationHelpers.UnauthenticatedAuthentication;

/// <summary>
/// Default values related to UnauthenticatedAuthentication handler.
/// </summary>
public static class UnauthenticatedAuthenticationDefaults
{
/// <summary>
/// The default value used for UnauthenticatedAuthenticationOptions.AuthenticationScheme.
/// </summary>
public const string AUTHENTICATIONSCHEME = "UnauthenticatedAuthentication";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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.UnauthenticatedAuthentication;

/// <summary>
/// This class is used to best integrate with ASP.NET Core AuthenticationHandler base class.
/// Ref: https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Core/src/AuthenticationHandler.cs
/// When "Unauthenticated" is configured, this handler authenticates the user as anonymous,
/// without reading any HTTP authentication headers.
/// </summary>
public class UnauthenticatedAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
/// <summary>
/// Constructor for the UnauthenticatedAuthenticationHandler.
/// Note the parameters are required by the base class.
/// </summary>
/// <param name="options">Authentication options.</param>
/// <param name="logger">Logger factory.</param>
/// <param name="encoder">URL encoder.</param>
public UnauthenticatedAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}

/// <summary>
/// Returns an unauthenticated ClaimsPrincipal for all requests.
/// The ClaimsPrincipal has no identity and no claims, representing an anonymous user.
/// </summary>
/// <returns>An authentication result to ASP.NET Core library authentication mechanisms</returns>
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// ClaimsIdentity without authenticationType means the user is not authenticated (anonymous)
ClaimsIdentity identity = new();
ClaimsPrincipal claimsPrincipal = new(identity);

AuthenticationTicket ticket = new(claimsPrincipal, UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to add UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME for the AuthenticationTicket if the in the ClaimsIdentity we don't use it since it is an unauthenticated user?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AuthenticationTicket requires a scheme name as part of the ASP.NET Core authentication infrastructure. While the ClaimsIdentity has no authenticationType (making it unauthenticated), the ticket still needs to identify which authentication handler processed the request. This follows the same pattern as SimulatorAuthenticationHandler.

AuthenticateResult success = AuthenticateResult.Success(ticket);
return Task.FromResult(success);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,25 @@ public void ValidateFailureWithUnneededEasyAuthConfig()
});
}

[TestMethod("Unauthenticated provider is correctly identified by IsUnauthenticatedAuthenticationProvider method")]
public void ValidateUnauthenticatedProviderIdentification()
{
// 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());
}

private static RuntimeConfig CreateRuntimeConfigWithOptionalAuthN(AuthenticationOptions authNConfig = null)
{
DataSource dataSource = new(DatabaseType.MSSQL, DEFAULT_CONNECTION_STRING, new());
Expand Down
11 changes: 9 additions & 2 deletions src/Service/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.UnauthenticatedAuthentication;
using Azure.DataApiBuilder.Core.Authorization;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
Expand Down Expand Up @@ -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 =>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -850,7 +856,8 @@ private static void ConfigureAuthenticationV2(IServiceCollection services, Runti
services.AddAuthentication()
.AddEnvDetectedEasyAuth()
.AddJwtBearer()
.AddSimulatorAuthentication();
.AddSimulatorAuthentication()
.AddUnauthenticatedAuthentication();
}

/// <summary>
Expand Down