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
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/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: []
}
55 changes: 55 additions & 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 Expand Up @@ -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);
}

/// <summary>
/// Test that the Unauthenticated provider is correctly identified by the IsUnauthenticatedAuthenticationProvider method.
/// </summary>
[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());
}

/// <summary>
/// Test that Unauthenticated provider does not require JWT configuration.
/// </summary>
[TestMethod]
public void TestUnauthenticatedProviderDoesNotRequireJwt()
{
AuthenticationOptions unauthenticatedOptions = new(Provider: "Unauthenticated");
Assert.IsFalse(unauthenticatedOptions.IsJwtConfiguredIdentityProvider());
}

/// <summary>
/// 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.
/// </summary>
[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");
}
}
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)
{
foreach (KeyValuePair<string, Entity> 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);
}
}
}
}
}

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 when all operations run as anonymous.</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.UnauthenticatedAuthenticationHandler;
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))
{
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.UnauthenticatedAuthenticationHandler;

/// <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.UnauthenticatedAuthenticationHandler;

/// <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,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;

/// <summary>
/// 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.
/// </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);
AuthenticateResult success = AuthenticateResult.Success(ticket);
return Task.FromResult(success);
}
}
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.UnauthenticatedAuthenticationHandler;
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
Loading