diff --git a/src/Configuration/src/CloudFoundry/CloudFoundryConfigurationProvider.cs b/src/Configuration/src/CloudFoundry/CloudFoundryConfigurationProvider.cs index 3ea65dbbe3..56e07e6508 100644 --- a/src/Configuration/src/CloudFoundry/CloudFoundryConfigurationProvider.cs +++ b/src/Configuration/src/CloudFoundry/CloudFoundryConfigurationProvider.cs @@ -73,6 +73,10 @@ private void Process() LoadData("vcap:application", applicationData.GetChildren(), data); AddDiegoVariables(data); + + // Enable evaluation of X-Forwarded headers so that ASP.NET Core works automatically behind Gorouter. + // Equivalent to setting ASPNETCORE_FORWARDEDHEADERS_ENABLED to true. + data["FORWARDEDHEADERS_ENABLED"] = "true"; } string? servicesJson = _settingsReader.ServicesJson; diff --git a/src/Configuration/test/CloudFoundry.Test/CloudfoundryConfigurationProviderTest.cs b/src/Configuration/test/CloudFoundry.Test/CloudfoundryConfigurationProviderTest.cs index 893402b696..a6578f63b6 100644 --- a/src/Configuration/test/CloudFoundry.Test/CloudfoundryConfigurationProviderTest.cs +++ b/src/Configuration/test/CloudFoundry.Test/CloudfoundryConfigurationProviderTest.cs @@ -2,9 +2,16 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +using System.Net; using FluentAssertions.Extensions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Steeltoe.Common.TestResources; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace Steeltoe.Configuration.CloudFoundry.Test; @@ -216,6 +223,102 @@ public void Load_VCAP_APPLICATION_Allows_Reload_Without_Throwing_Exception() Assert.Equal("fb8fbcc6-8d58-479e-bcc7-3b4ce5a7f0ca", options.Version); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ForwardedHeadersOptions_unrestricted_when_running_on_CloudFoundry(bool isRunningOnCloudFoundry) + { + using IDisposable? scope = isRunningOnCloudFoundry ? new EnvironmentVariableScope("VCAP_APPLICATION", "{}") : null; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(); + builder.AddCloudFoundryConfiguration(); + await using WebApplication host = builder.Build(); + + ForwardedHeadersOptions options = host.Services.GetRequiredService>().Value; + + if (isRunningOnCloudFoundry) + { + options.ForwardedHeaders.Should().HaveFlag(ForwardedHeaders.XForwardedFor); + options.ForwardedHeaders.Should().HaveFlag(ForwardedHeaders.XForwardedProto); + options.KnownNetworks.Should().BeEmpty(); + options.KnownProxies.Should().BeEmpty(); + } + else + { + options.ForwardedHeaders.Should().NotHaveFlag(ForwardedHeaders.XForwardedFor); + options.ForwardedHeaders.Should().NotHaveFlag(ForwardedHeaders.XForwardedProto); + options.KnownNetworks.Should().ContainSingle().Which.Should().BeEquivalentTo(IPNetwork.Parse("127.0.0.1/8")); + options.KnownProxies.Should().ContainSingle().Which.Should().Be(IPAddress.Parse("::1")); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ForwardedHeadersMiddleware_updates_connection_details_when_running_on_CloudFoundry(bool isRunningOnCloudFoundry) + { + using IDisposable? scope = isRunningOnCloudFoundry ? new EnvironmentVariableScope("VCAP_APPLICATION", "{}") : null; + + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseKestrel().UseUrls("http://127.0.0.1:0"); + builder.AddCloudFoundryConfiguration(); + await using WebApplication host = builder.Build(); + bool? forwardedHeadersWereEvaluated = null; + + host.Map("/", context => + { + forwardedHeadersWereEvaluated = context.Request.IsHttps; + return Task.CompletedTask; + }); + + await host.StartAsync(TestContext.Current.CancellationToken); + string address = host.Urls.First(url => url.StartsWith("http://", StringComparison.OrdinalIgnoreCase)); + + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-Forwarded-Proto", "https"); + client.DefaultRequestHeaders.Add("X-Forwarded-For", "1.2.3.4"); + + HttpResponseMessage response = await client.GetAsync(new Uri(address), TestContext.Current.CancellationToken); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + forwardedHeadersWereEvaluated.Should() + .Be(isRunningOnCloudFoundry, $"X-Forwarded-Proto should {(isRunningOnCloudFoundry ? string.Empty : "not ")}be evaluated"); + + await host.StopAsync(TestContext.Current.CancellationToken); + } + + [Fact] + public async Task ForwardedHeadersMiddleware_uses_customized_options_when_running_on_CloudFoundry() + { + using var vcapScope = new EnvironmentVariableScope("VCAP_APPLICATION", "{}"); + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseKestrel().UseUrls("http://127.0.0.1:0"); + builder.AddCloudFoundryConfiguration(); + builder.Services.Configure(options => options.KnownProxies.Add(IPAddress.Parse("192.168.1.20"))); + await using WebApplication host = builder.Build(); + bool? forwardedHeadersWereEvaluated = null; + + host.Map("/", context => + { + forwardedHeadersWereEvaluated = context.Request.IsHttps; + return Task.CompletedTask; + }); + + await host.StartAsync(TestContext.Current.CancellationToken); + string address = host.Urls.First(url => url.StartsWith("http://", StringComparison.OrdinalIgnoreCase)); + + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-Forwarded-Proto", "https"); + client.DefaultRequestHeaders.Add("X-Forwarded-For", "1.2.3.4"); + + HttpResponseMessage response = await client.GetAsync(new Uri(address), TestContext.Current.CancellationToken); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + forwardedHeadersWereEvaluated.Should().BeFalse("X-Forwarded-Proto should not be evaluated for unknown proxies"); + + await host.StopAsync(TestContext.Current.CancellationToken); + } + private sealed class VcapApp { #pragma warning disable S3459 // Unassigned members should be removed diff --git a/src/Security/src/Authorization.Certificate/CertificateApplicationBuilderExtensions.cs b/src/Security/src/Authorization.Certificate/CertificateApplicationBuilderExtensions.cs index f51bec47a6..446a3337e6 100644 --- a/src/Security/src/Authorization.Certificate/CertificateApplicationBuilderExtensions.cs +++ b/src/Security/src/Authorization.Certificate/CertificateApplicationBuilderExtensions.cs @@ -3,47 +3,25 @@ // See the LICENSE file in the project root for more information. using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.HttpOverrides; namespace Steeltoe.Security.Authorization.Certificate; public static class CertificateApplicationBuilderExtensions { - /// - /// Enables certificate and header forwarding, along with ASP.NET Core authentication and authorization middlewares. Sets ForwardedHeaders to - /// . - /// - /// - /// The to configure. - /// - /// - /// The incoming so that additional calls can be chained. - /// - public static IApplicationBuilder UseCertificateAuthorization(this IApplicationBuilder builder) - { - return UseCertificateAuthorization(builder, new ForwardedHeadersOptions()); - } - /// /// Enables certificate and header forwarding, along with ASP.NET Core authentication and authorization middlewares. /// /// /// The to configure. /// - /// - /// Custom header forwarding policy. is added to your . - /// /// /// The incoming so that additional calls can be chained. /// - public static IApplicationBuilder UseCertificateAuthorization(this IApplicationBuilder builder, ForwardedHeadersOptions options) + public static IApplicationBuilder UseCertificateAuthorization(this IApplicationBuilder builder) { ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(options); - - options.ForwardedHeaders |= ForwardedHeaders.XForwardedProto; - builder.UseForwardedHeaders(options); + builder.UseForwardedHeaders(); builder.UseCertificateForwarding(); builder.UseAuthentication(); builder.UseAuthorization(); diff --git a/src/Security/src/Authorization.Certificate/PublicAPI.Unshipped.txt b/src/Security/src/Authorization.Certificate/PublicAPI.Unshipped.txt index 140d00ac24..99e1ff153e 100644 --- a/src/Security/src/Authorization.Certificate/PublicAPI.Unshipped.txt +++ b/src/Security/src/Authorization.Certificate/PublicAPI.Unshipped.txt @@ -2,7 +2,6 @@ const Steeltoe.Security.Authorization.Certificate.CertificateAuthorizationPolicies.SameOrg = "sameorg" -> string! const Steeltoe.Security.Authorization.Certificate.CertificateAuthorizationPolicies.SameSpace = "samespace" -> string! static Steeltoe.Security.Authorization.Certificate.CertificateApplicationBuilderExtensions.UseCertificateAuthorization(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! -static Steeltoe.Security.Authorization.Certificate.CertificateApplicationBuilderExtensions.UseCertificateAuthorization(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder, Microsoft.AspNetCore.Builder.ForwardedHeadersOptions! options) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Steeltoe.Security.Authorization.Certificate.CertificateAuthorizationBuilderExtensions.AddOrgAndSpacePolicies(this Microsoft.AspNetCore.Authorization.AuthorizationBuilder! builder) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! static Steeltoe.Security.Authorization.Certificate.CertificateAuthorizationBuilderExtensions.AddOrgAndSpacePolicies(this Microsoft.AspNetCore.Authorization.AuthorizationBuilder! builder, string? certificateHeaderName) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! static Steeltoe.Security.Authorization.Certificate.CertificateAuthorizationPolicyBuilderExtensions.RequireSameOrg(this Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder! builder) -> Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!