feat/Update_Controllers_to_Allow_for_Premium_Membership
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
using System.Security.Claims;
|
||||
using System.Net;
|
||||
using JobTrackerApi.Controllers;
|
||||
using JobTrackerApi.Services;
|
||||
using JobTrackerApi.Tests.TestSupport;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.Tests;
|
||||
|
||||
public sealed class ExternalOriginTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData("http://jobs.example.test")]
|
||||
[InlineData("https://user@jobs.example.test")]
|
||||
[InlineData("https://jobs.example.test/path")]
|
||||
[InlineData("https://jobs.example.test?query=1")]
|
||||
[InlineData("https://jobs.example.test#fragment")]
|
||||
public void Production_requires_one_clean_https_origin(string? value)
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => ExternalOrigin.Parse(value, production: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Development_defaults_to_local_frontend()
|
||||
{
|
||||
Assert.Equal("http://localhost:3000", ExternalOrigin.Parse(null, production: false).BaseUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Canonical_host_matching_includes_the_configured_port()
|
||||
{
|
||||
var origin = ExternalOrigin.Parse("https://jobs.example.test:8443/", production: true);
|
||||
|
||||
Assert.True(origin.Matches(new HostString("jobs.example.test", 8443)));
|
||||
Assert.False(origin.Matches(new HostString("jobs.example.test")));
|
||||
Assert.False(origin.Matches(new HostString("attacker.example.test", 8443)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Internal_host_is_allowed_only_for_liveness()
|
||||
{
|
||||
var origin = ExternalOrigin.Parse("https://jobs.example.test", production: true);
|
||||
|
||||
Assert.True(origin.AllowsRequest(new HostString("jobs.example.test"), "/api/auth/config"));
|
||||
Assert.True(origin.AllowsRequest(new HostString("localhost", 8080), "/health"));
|
||||
Assert.False(origin.AllowsRequest(new HostString("localhost", 8080), "/api/auth/config"));
|
||||
Assert.False(origin.AllowsRequest(new HostString("attacker.example.test"), "/health"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Forwarded_headers_require_an_explicit_proxy_network()
|
||||
{
|
||||
var missing = BuildConfig(new Dictionary<string, string?>());
|
||||
var invalid = BuildConfig(new Dictionary<string, string?> { ["Proxy:KnownNetworks:0"] = "anywhere" });
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => ForwardedProxyConfiguration.Build(missing));
|
||||
Assert.Throws<InvalidOperationException>(() => ForwardedProxyConfiguration.Build(invalid));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Forwarded_headers_trust_one_hop_from_the_configured_network_only()
|
||||
{
|
||||
var config = BuildConfig(new Dictionary<string, string?> { ["Proxy:KnownNetworks:0"] = "172.31.250.0/29" });
|
||||
|
||||
var options = ForwardedProxyConfiguration.Build(config);
|
||||
|
||||
Assert.Equal(1, options.ForwardLimit);
|
||||
var network = Assert.Single(options.KnownNetworks);
|
||||
Assert.True(network.Contains(IPAddress.Parse("172.31.250.2")));
|
||||
Assert.False(network.Contains(IPAddress.Parse("172.31.251.2")));
|
||||
Assert.Empty(options.KnownProxies);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OAuth_callback_ignores_request_host_and_legacy_redirect_override()
|
||||
{
|
||||
var config = BuildConfig(new Dictionary<string, string?>
|
||||
{
|
||||
["App:PublicBaseUrl"] = "https://jobs.example.test",
|
||||
["Microsoft:RedirectUri"] = "https://attacker.example.test/callback",
|
||||
});
|
||||
var graph = new Mock<IMicrosoftGraphOAuthService>();
|
||||
graph.Setup(x => x.BuildAuthorizationUrl("user-1", "https://jobs.example.test/api/microsoft-graph/oauth/callback"))
|
||||
.Returns("https://login.microsoftonline.com/authorize");
|
||||
var controller = new MicrosoftGraphController(graph.Object, config)
|
||||
{
|
||||
ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext
|
||||
{
|
||||
User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, "user-1"),
|
||||
}, "test")),
|
||||
},
|
||||
},
|
||||
};
|
||||
controller.Request.Host = new HostString("attacker.example.test");
|
||||
|
||||
controller.ConnectUrl();
|
||||
|
||||
graph.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Csrf_cookie_security_comes_from_canonical_origin()
|
||||
{
|
||||
var config = BuildConfig(new Dictionary<string, string?> { ["App:PublicBaseUrl"] = "https://jobs.example.test" });
|
||||
var controller = new AuthController(
|
||||
config,
|
||||
TestHostFactory.CreateUserManager().Object,
|
||||
Mock.Of<ITokenService>(),
|
||||
Mock.Of<IAppEmailSender>(),
|
||||
Mock.Of<IGoogleTokenValidator>(),
|
||||
Mock.Of<IMicrosoftTokenValidator>(),
|
||||
NullLogger<AuthController>.Instance,
|
||||
Mock.Of<ITwoFactorPendingTokenService>(),
|
||||
TestHostFactory.CreateInMemoryDb())
|
||||
{
|
||||
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() },
|
||||
};
|
||||
controller.Request.Scheme = "http";
|
||||
controller.Request.Headers["X-Forwarded-Proto"] = "http";
|
||||
|
||||
controller.EnsureCsrfCookie();
|
||||
|
||||
Assert.Contains("secure", controller.Response.Headers.SetCookie.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static IConfiguration BuildConfig(IDictionary<string, string?> values) =>
|
||||
new ConfigurationBuilder().AddInMemoryCollection(values).Build();
|
||||
}
|
||||
Reference in New Issue
Block a user