feat(api): honour X-Forwarded-Proto so Secure cookies flip on behind Traefik
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using VolcanicCore.Api;
|
||||
using VolcanicCore.Api.Auth;
|
||||
using VolcanicCore.Api.Endpoints;
|
||||
@@ -107,6 +108,17 @@ builder.Services
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// Trust Coolify's Traefik as the TLS-terminating proxy. Clearing the default
|
||||
// KnownNetworks/KnownProxies is required because the container talks to Traefik
|
||||
// over the compose network — that IP is not in the built-in localhost list.
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders =
|
||||
ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor;
|
||||
options.KnownNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
});
|
||||
|
||||
// Reflection scan — pick up INodeType / IConnectionProvider implementations
|
||||
// registered as DI services by any loaded assembly. In Plan 2, node/provider
|
||||
// assemblies will register their types via extension methods.
|
||||
@@ -155,6 +167,9 @@ using (var scope = app.Services.CreateScope())
|
||||
AdminBootstrap.Seed(repo, pw, settings.AdminPassword);
|
||||
}
|
||||
|
||||
// Must run before UseAuthentication so cookie SecurePolicy.SameAsRequest sees https.
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
64
tests/VolcanicCore.Tests/ForwardedHeadersTests.cs
Normal file
64
tests/VolcanicCore.Tests/ForwardedHeadersTests.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace VolcanicCore.Tests;
|
||||
|
||||
public class ForwardedHeadersTests : IClassFixture<WebFactory>
|
||||
{
|
||||
private readonly WebFactory _f;
|
||||
public ForwardedHeadersTests(WebFactory f) { _f = f; }
|
||||
|
||||
[Fact]
|
||||
public async Task Login_with_XForwardedProto_https_returns_Secure_cookie()
|
||||
{
|
||||
// AllowAutoRedirect false so we can inspect the raw Set-Cookie header.
|
||||
var client = _f.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions
|
||||
{
|
||||
AllowAutoRedirect = false,
|
||||
});
|
||||
// Simulate Traefik terminating TLS: it forwards over plain HTTP with the
|
||||
// scheme and client IP in headers.
|
||||
client.DefaultRequestHeaders.Add("X-Forwarded-Proto", "https");
|
||||
client.DefaultRequestHeaders.Add("X-Forwarded-For", "203.0.113.7");
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, "/api/session")
|
||||
{
|
||||
Content = JsonContent.Create(new { password = _f.AdminPassword }),
|
||||
};
|
||||
var res = await client.SendAsync(req);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, res.StatusCode);
|
||||
|
||||
// Cookie should be marked Secure because the middleware saw https.
|
||||
var setCookies = res.Headers.GetValues("Set-Cookie").ToList();
|
||||
var sid = setCookies.FirstOrDefault(c => c.StartsWith("volcaniccore.sid=", StringComparison.Ordinal));
|
||||
Assert.NotNull(sid);
|
||||
Assert.Contains("secure", sid!, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_without_XForwardedProto_returns_cookie_without_Secure()
|
||||
{
|
||||
// Same request WITHOUT the forwarded header — Request.Scheme stays http,
|
||||
// so CookieSecurePolicy.SameAsRequest omits the Secure flag. This proves
|
||||
// the middleware is doing the work; without it the "https" test above
|
||||
// would still pass because both requests would look like http.
|
||||
var client = _f.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions
|
||||
{
|
||||
AllowAutoRedirect = false,
|
||||
});
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, "/api/session")
|
||||
{
|
||||
Content = JsonContent.Create(new { password = _f.AdminPassword }),
|
||||
};
|
||||
var res = await client.SendAsync(req);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, res.StatusCode);
|
||||
var setCookies = res.Headers.GetValues("Set-Cookie").ToList();
|
||||
var sid = setCookies.FirstOrDefault(c => c.StartsWith("volcaniccore.sid=", StringComparison.Ordinal));
|
||||
Assert.NotNull(sid);
|
||||
Assert.DoesNotContain("secure", sid!, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user