Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<IOptions<ForwardedHeadersOptions>>().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<ForwardedHeadersOptions>(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// Enables certificate and header forwarding, along with ASP.NET Core authentication and authorization middlewares. Sets ForwardedHeaders to
/// <see cref="ForwardedHeaders.XForwardedProto" />.
/// </summary>
/// <param name="builder">
/// The <see cref="IApplicationBuilder" /> to configure.
/// </param>
/// <returns>
/// The incoming <paramref name="builder" /> so that additional calls can be chained.
/// </returns>
public static IApplicationBuilder UseCertificateAuthorization(this IApplicationBuilder builder)
{
return UseCertificateAuthorization(builder, new ForwardedHeadersOptions());
}

/// <summary>
/// Enables certificate and header forwarding, along with ASP.NET Core authentication and authorization middlewares.
/// </summary>
/// <param name="builder">
/// The <see cref="IApplicationBuilder" /> to configure.
/// </param>
/// <param name="options">
/// Custom header forwarding policy. <see cref="ForwardedHeaders.XForwardedProto" /> is added to your <see cref="ForwardedHeadersOptions" />.
/// </param>
/// <returns>
/// The incoming <paramref name="builder" /> so that additional calls can be chained.
/// </returns>
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down