Skip to content

Commit f44320f

Browse files
authored
Merge branch 'main' into copilot/add-cli-parameter-for-health-name
2 parents 020c18e + b3efb1a commit f44320f

5 files changed

Lines changed: 88 additions & 4 deletions

File tree

src/Directory.Packages.props

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="9.5.2" />
88
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="9.5.2" />
99
<PackageVersion Include="Azure.Core" Version="1.47.1" />
10-
<PackageVersion Include="Azure.Identity" Version="1.15.0" />
10+
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
1111
<PackageVersion Include="Azure.Monitor.Ingestion" Version="1.1.2" />
1212
<PackageVersion Include="Azure.Security.KeyVault.Secrets" Version="4.6.0" />
1313
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
@@ -34,6 +34,7 @@
3434
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
3535
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="8.0.10" />
3636
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.38.1" />
37+
<PackageVersion Include="Microsoft.Azure.StackExchangeRedis" Version="3.3.1" />
3738
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
3839
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.0" />
3940
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="9.0.0" />
@@ -46,7 +47,7 @@
4647
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
4748
<!--When updating Microsoft.Data.SqlClient, update license URL in scripts/notice-generation.ps1-->
4849
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.3" />
49-
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
50+
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
5051
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
5152
<PackageVersion Include="Microsoft.Extensions.Logging.ApplicationInsights" Version="2.22.0" />
5253
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.1.2" />

src/Service.Tests/Authentication/JwtTokenAuthenticationUnitTests.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,15 @@ public async Task TestInvalidToken_BadAudience()
173173
Assert.AreEqual(expected: (int)HttpStatusCode.Unauthorized, actual: postMiddlewareContext.Response.StatusCode);
174174
Assert.IsFalse(postMiddlewareContext.User.Identity.IsAuthenticated);
175175
StringValues headerValue = GetChallengeHeader(postMiddlewareContext);
176-
Assert.IsTrue(headerValue[0].Contains("invalid_token") && headerValue[0].Contains($"The audience '{BAD_AUDIENCE}' is invalid"));
176+
177+
// Microsoft.IdentityModel.Tokens version 8.8+ scrubs the Audience from the error message
178+
// This behavior can be disabled with AppContext.SetSwitch("Switch.Microsoft.IdentityModel.DoNotScrubExceptions", true);
179+
// See https://aka.ms/identitymodel/app-context-switches
180+
string expectedAudienceInErrorMessage = AppContext.TryGetSwitch("Switch.Microsoft.IdentityModel.DoNotScrubExceptions", out bool isExceptionScrubbingDisabled) && isExceptionScrubbingDisabled
181+
? BAD_AUDIENCE
182+
: "(null)";
183+
184+
Assert.IsTrue(headerValue[0].Contains("invalid_token") && headerValue[0].Contains($"The audience '{expectedAudienceInErrorMessage}' is invalid"));
177185
}
178186

179187
/// <summary>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
using StackExchange.Redis;
6+
7+
namespace Azure.DataApiBuilder.Service.Tests.UnitTests
8+
{
9+
[TestClass]
10+
public class StartupTests
11+
{
12+
[DataTestMethod]
13+
[DataRow("localhost:6379", false, DisplayName = "Localhost endpoint without password should NOT use Entra auth.")]
14+
[DataRow("127.0.0.1:6379", false, DisplayName = "IPv4 loopback without password should NOT use Entra auth.")]
15+
[DataRow("[::1]:6379", false, DisplayName = "IPv6 loopback without password should NOT use Entra auth.")]
16+
[DataRow("redis.example.com:6380", true, DisplayName = "Remote endpoint without password SHOULD use Entra auth.")]
17+
[DataRow("redis.example.com:6380,password=secret", false, DisplayName = "Presence of password should NOT use Entra auth, even for remote endpoints.")]
18+
[DataRow("localhost:6379,redis.example.com:6380", true, DisplayName = "Mixed endpoints (including remote) without password SHOULD use Entra auth.")]
19+
[DataRow("localhost:6379,password=secret", false, DisplayName = "Localhost with password should NOT use Entra auth.")]
20+
public void ShouldUseEntraAuthForRedis(string connectionString, bool expectedUseEntraAuth)
21+
{
22+
// Arrange
23+
var options = ConfigurationOptions.Parse(connectionString);
24+
25+
// Act
26+
bool result = Startup.ShouldUseEntraAuthForRedis(options);
27+
28+
// Assert
29+
Assert.AreEqual(expectedUseEntraAuth, result);
30+
}
31+
}
32+
}

src/Service/Azure.DataApiBuilder.Service.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" />
6565
<PackageReference Include="Microsoft.Extensions.Logging.ApplicationInsights" />
6666
<PackageReference Include="Microsoft.Azure.Cosmos" />
67+
<PackageReference Include="Microsoft.Azure.StackExchangeRedis" />
6768
<PackageReference Include="Microsoft.Data.SqlClient" />
6869
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
6970
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />

src/Service/Startup.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System;
55
using System.IO.Abstractions;
6+
using System.Linq;
7+
using System.Net;
68
using System.Net.Http;
79
using System.Net.Http.Headers;
810
using System.Threading.Tasks;
@@ -435,7 +437,7 @@ public void ConfigureServices(IServiceCollection services)
435437
else
436438
{
437439
// NOTE: this is done to reuse the same connection multiplexer for both the cache and backplane
438-
Task<ConnectionMultiplexer> connectionMultiplexerTask = ConnectionMultiplexer.ConnectAsync(level2CacheOptions.ConnectionString);
440+
Task<IConnectionMultiplexer> connectionMultiplexerTask = CreateConnectionMultiplexerAsync(level2CacheOptions.ConnectionString);
439441

440442
fusionCacheBuilder
441443
.WithSerializer(new FusionCacheSystemTextJsonSerializer())
@@ -473,6 +475,46 @@ public void ConfigureServices(IServiceCollection services)
473475
services.AddControllers();
474476
}
475477

478+
/// <summary>
479+
/// Creates a ConnectionMultiplexer for Redis with support for Azure Entra authentication.
480+
/// </summary>
481+
/// <param name="connectionString">The Redis connection string.</param>
482+
/// <returns>A task that represents the asynchronous operation. The task result contains the connected IConnectionMultiplexer.</returns>
483+
private static async Task<IConnectionMultiplexer> CreateConnectionMultiplexerAsync(string connectionString)
484+
{
485+
ConfigurationOptions options = ConfigurationOptions.Parse(connectionString);
486+
487+
if (ShouldUseEntraAuthForRedis(options))
488+
{
489+
options = await options.ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential());
490+
}
491+
492+
return await ConnectionMultiplexer.ConnectAsync(options);
493+
}
494+
495+
/// <summary>
496+
/// Determines whether Azure Entra authentication should be used.
497+
/// Conditions:
498+
/// - No password provided
499+
/// - At least one endpoint is NOT localhost/loopback
500+
/// </summary>
501+
/// <param name="options">The Redis configuration options.</param>
502+
/// <returns>True if Azure Entra authentication should be used; otherwise, false.</returns>
503+
/// <remarks>Internal for testing.</remarks>
504+
internal static bool ShouldUseEntraAuthForRedis(ConfigurationOptions options)
505+
{
506+
// Determine if an endpoint is localhost/loopback
507+
static bool IsLocalhostEndpoint(EndPoint ep) => ep switch
508+
{
509+
DnsEndPoint dns => string.Equals(dns.Host, "localhost", StringComparison.OrdinalIgnoreCase),
510+
IPEndPoint ip => IPAddress.IsLoopback(ip.Address),
511+
_ => false,
512+
};
513+
514+
return string.IsNullOrEmpty(options.Password)
515+
&& options.EndPoints.Any(ep => !IsLocalhostEndpoint(ep));
516+
}
517+
476518
/// <summary>
477519
/// Configures HTTP response compression based on the runtime configuration.
478520
/// Compression is applied at the middleware level and supports Gzip and Brotli.

0 commit comments

Comments
 (0)