Test: a 307 from Heimdall is not followed by the auth and member clients (red)
AddHeimdallClients gathers the three Heimdall client registrations so the test exercises the registrations Program uses. The auth and member clients still follow redirects here; the mover already does not. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2d9eb868bf
commit
0599aedf08
3 changed files with 94 additions and 10 deletions
|
|
@ -313,8 +313,23 @@ public sealed partial class HeimdallIdentityMoveClient(
|
|||
}
|
||||
}
|
||||
|
||||
public static class HeimdallIdentityMoveClientServices
|
||||
public static class HeimdallClientServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Every Heimdall client Bifrost owns. The auth client carries the app secret, the member client only the member's bearer and
|
||||
/// the mover the app secret and its call deadline; all share the header redaction.
|
||||
/// </summary>
|
||||
public static void AddHeimdallClients(this IServiceCollection services)
|
||||
{
|
||||
services.AddHttpClient<HeimdallAuthService>((serviceProvider, client) =>
|
||||
HeimdallAuthService.ConfigureClient(client, serviceProvider.GetRequiredService<IOptions<HeimdallOptions>>().Value))
|
||||
.RedactLoggedHeaders(HeimdallAuthService.RedactsHeader);
|
||||
services.AddHttpClient<HeimdallMemberClient>((serviceProvider, client) =>
|
||||
HeimdallMemberClient.ConfigureClient(client, serviceProvider.GetRequiredService<IOptions<HeimdallOptions>>().Value))
|
||||
.RedactLoggedHeaders(HeimdallAuthService.RedactsHeader);
|
||||
services.AddHeimdallIdentityMoveClient();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The mover's HttpClient: Heimdall's base address and app secret from HeimdallAuthService.ConfigureClient (the one secret path), the
|
||||
/// call deadline, no redirects (a 3xx must never carry the secret or the proofs to another origin), and the shared header redaction.
|
||||
|
|
|
|||
|
|
@ -154,15 +154,8 @@ builder.Services.AddScoped<ReadinessService>();
|
|||
builder.Services.AddScoped<ApplicationBootstrapper>();
|
||||
builder.Services.AddSingleton<StartupConfigurationValidator>();
|
||||
builder.Services.AddScoped<IAuthorizationHandler, ActiveMemberAuthorizationHandler>();
|
||||
builder.Services.AddHttpClient<HeimdallAuthService>((serviceProvider, client) =>
|
||||
HeimdallAuthService.ConfigureClient(client, serviceProvider.GetRequiredService<IOptions<HeimdallOptions>>().Value))
|
||||
.RedactLoggedHeaders(HeimdallAuthService.RedactsHeader);
|
||||
// Heimdall's member routes take only the member's bearer: this client carries no app secret.
|
||||
builder.Services.AddHttpClient<HeimdallMemberClient>((serviceProvider, client) =>
|
||||
HeimdallMemberClient.ConfigureClient(client, serviceProvider.GetRequiredService<IOptions<HeimdallOptions>>().Value))
|
||||
.RedactLoggedHeaders(HeimdallAuthService.RedactsHeader);
|
||||
// The mover route is an app-secret call like HeimdallAuthService's: same ConfigureClient, same header redaction, no redirects.
|
||||
builder.Services.AddHeimdallIdentityMoveClient();
|
||||
// The app-secret and member-bearer clients and the mover: no redirects (see HeimdallClientServices).
|
||||
builder.Services.AddHeimdallClients();
|
||||
builder.Services.AddSingleton<LinkedAccountsSnapshots>();
|
||||
builder.Services.AddSingleton<Bifrost.Web.Features.Passkeys.PasskeyRelyingParty>();
|
||||
builder.Services.AddSingleton<Bifrost.Web.Features.Passkeys.PasskeyCeremonies>();
|
||||
|
|
|
|||
76
tests/Bifrost.Web.Tests/HeimdallRedirectTests.cs
Normal file
76
tests/Bifrost.Web.Tests/HeimdallRedirectTests.cs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
using System.Net;
|
||||
using Bifrost.Web.Configuration;
|
||||
using Bifrost.Web.Features.Heimdall;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bifrost.Web.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Every Heimdall client as Program registers it (<see cref="HeimdallClientServices.AddHeimdallClients"/>): a 3xx from Heimdall's origin
|
||||
/// is the answer, never a second request. The app secret and a member's bearer reach only the origin they were configured for.
|
||||
/// </summary>
|
||||
public sealed class HeimdallRedirectTests
|
||||
{
|
||||
private static int FreePort()
|
||||
{
|
||||
var listener = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(HeimdallAuthService), 307)]
|
||||
[InlineData(typeof(HeimdallAuthService), 302)]
|
||||
[InlineData(typeof(HeimdallMemberClient), 307)]
|
||||
[InlineData(typeof(HeimdallMemberClient), 302)]
|
||||
[InlineData(typeof(HeimdallIdentityMoveClient), 307)]
|
||||
public async Task A_redirect_from_Heimdall_is_not_followed_and_no_secret_or_bearer_reaches_the_other_host(Type client, int code)
|
||||
{
|
||||
int first = FreePort(), second = FreePort();
|
||||
using var origin = new HttpListener();
|
||||
origin.Prefixes.Add($"http://127.0.0.1:{first}/");
|
||||
origin.Start();
|
||||
using var other = new HttpListener();
|
||||
other.Prefixes.Add($"http://127.0.0.1:{second}/");
|
||||
other.Start();
|
||||
var redirected = Task.Run(async () =>
|
||||
{
|
||||
var context = await origin.GetContextAsync();
|
||||
context.Response.StatusCode = code;
|
||||
context.Response.RedirectLocation = $"http://127.0.0.1:{second}/steal";
|
||||
context.Response.Close();
|
||||
});
|
||||
var stolen = new List<string>();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var context = await other.GetContextAsync();
|
||||
stolen.Add(context.Request.Headers["x-heimdall-app-secret"] + context.Request.Headers["Authorization"]);
|
||||
context.Response.StatusCode = 200;
|
||||
context.Response.Close();
|
||||
}
|
||||
catch (Exception error) when (error is HttpListenerException or ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
});
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddSingleton(Options.Create(new HeimdallOptions { BaseUrl = $"http://127.0.0.1:{first}", AppSecret = "APP-SECRET-VALUE" }));
|
||||
services.AddHeimdallClients();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
using var http = provider.GetRequiredService<IHttpClientFactory>().CreateClient(client.Name);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/v1/me/identities");
|
||||
request.Headers.Authorization = new("Bearer", "MEMBER-BEARER");
|
||||
|
||||
using var response = await http.SendAsync(request);
|
||||
await redirected;
|
||||
await Task.Delay(300);
|
||||
|
||||
Assert.Equal(code, (int)response.StatusCode);
|
||||
Assert.Empty(stolen);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue