mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Merge pull request #2341 from DuendeSoftware/ev/bff/fix-htu
fix issue where yarp proxying doesn't calculate htu correctly
This commit is contained in:
commit
b8fd3cb8a9
2 changed files with 113 additions and 3 deletions
|
|
@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Http;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Yarp.ReverseProxy.Forwarder;
|
||||
using Yarp.ReverseProxy.Model;
|
||||
using Yarp.ReverseProxy.Transforms;
|
||||
|
||||
|
|
@ -156,14 +157,17 @@ internal class AccessTokenRequestTransform(
|
|||
|
||||
private async Task ApplyDPoPToken(RequestTransformContext context, DPoPTokenResult token)
|
||||
{
|
||||
var url = RequestUtilities.MakeDestinationAddress(
|
||||
context.DestinationPrefix,
|
||||
context.Path,
|
||||
QueryString.Empty);
|
||||
|
||||
var baseUri = new Uri(context.DestinationPrefix);
|
||||
var proofToken = await proofService.CreateProofTokenAsync(new DPoPProofRequest
|
||||
{
|
||||
AccessToken = token.AccessToken,
|
||||
DPoPProofKey = token.DPoPJsonWebKey,
|
||||
Method = context.ProxyRequest.Method,
|
||||
Url = new Uri(baseUri, context.Path)
|
||||
Url = url
|
||||
});
|
||||
if (proofToken != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -80,6 +80,112 @@ public class DPoPRemoteEndpointTests(ITestOutputHelper output) : BffTestBase(out
|
|||
|
||||
callToApi.RequestHeaders["DPoP"].First().ShouldNotBeNullOrEmpty();
|
||||
callToApi.RequestHeaders["Authorization"].First().StartsWith("DPoP ").ShouldBeTrue();
|
||||
callToApi.ClientId.ShouldNotBeNullOrEmpty();
|
||||
callToApi.ClientId.ShouldNotBeNullOrEmpty("this clientid would be empty if dpop validation failes");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/api/foo", "api/foo", "api/foo/bar")]
|
||||
[InlineData("/api/foo", "api/foo", "api/foo")]
|
||||
[InlineData("/api/foo", "foo", "api/foo")]
|
||||
public async Task HTU_values_are_correctly_verified_for_paths_and_subpaths(string mappedPath, string targetPath, string calledPath)
|
||||
{
|
||||
Api.OnConfigureServices += services =>
|
||||
{
|
||||
services.ConfigureDPoPTokensForScheme("token");
|
||||
};
|
||||
|
||||
Bff.OnConfigureApp += app =>
|
||||
{
|
||||
app.MapRemoteBffApiEndpoint(mappedPath, Api.Url(targetPath))
|
||||
.WithAccessToken(RequiredTokenType.Client);
|
||||
};
|
||||
|
||||
await InitializeAsync();
|
||||
|
||||
ApiCallDetails callToApi = await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(calledPath)
|
||||
);
|
||||
|
||||
callToApi.RequestHeaders["DPoP"].First().ShouldNotBeNullOrEmpty();
|
||||
callToApi.RequestHeaders["Authorization"].First().StartsWith("DPoP ").ShouldBeTrue();
|
||||
callToApi.ClientId.ShouldNotBeNullOrEmpty("this clientid would be empty if dpop validation failes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DPoP_htu_matches_yarp_destination_when_api_has_path_prefix()
|
||||
{
|
||||
// This test reproduces the issue described in https://github.com/orgs/DuendeSoftware/discussions/461
|
||||
// When mapping /api/foo to https://example.com/api/foo, a request to /api/foo/bar should
|
||||
// result in DPoP HTU of https://example.com/api/foo/bar (not https://example.com/bar)
|
||||
|
||||
string? capturedDPoPHeader = null;
|
||||
string? capturedRequestPath = null;
|
||||
|
||||
Api.OnConfigureServices += services =>
|
||||
{
|
||||
|
||||
services.ConfigureDPoPTokensForScheme("token");
|
||||
};
|
||||
|
||||
Api.OnConfigureApp += app =>
|
||||
{
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
capturedDPoPHeader = context.Request.Headers["DPoP"].FirstOrDefault();
|
||||
capturedRequestPath = context.Request.Path.Value;
|
||||
await next();
|
||||
});
|
||||
};
|
||||
|
||||
Bff.OnConfigureApp += app =>
|
||||
{
|
||||
// Map BFF /api/foo to API https://localhost:port/api/foo
|
||||
app.MapRemoteBffApiEndpoint("/api/foo", Api.Url("api/foo"))
|
||||
.WithAccessToken(RequiredTokenType.Client);
|
||||
};
|
||||
|
||||
await InitializeAsync();
|
||||
|
||||
// Make a request to /api/foo/bar
|
||||
ApiCallDetails callToApi = await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url("/api/foo/bar")
|
||||
);
|
||||
|
||||
// Verify the API received the request at the correct path
|
||||
capturedRequestPath.ShouldBe("/api/foo/bar");
|
||||
|
||||
// Parse the DPoP proof token to extract the HTU claim
|
||||
capturedDPoPHeader.ShouldNotBeNullOrEmpty();
|
||||
var dpopToken = capturedDPoPHeader!;
|
||||
|
||||
// The DPoP token is a JWT with format: header.payload.signature
|
||||
var parts = dpopToken.Split('.');
|
||||
parts.Length.ShouldBe(3);
|
||||
|
||||
// Decode the payload (base64url)
|
||||
var payload = System.Text.Json.JsonDocument.Parse(
|
||||
Convert.FromBase64String(Base64UrlDecode(parts[1]))
|
||||
);
|
||||
|
||||
var htu = payload.RootElement.GetProperty("htu").GetString();
|
||||
|
||||
// The HTU should match the actual destination URL that YARP sent the request to
|
||||
// Expected: https://localhost:port/api/foo/bar
|
||||
// Bug would produce: https://localhost:port/bar
|
||||
htu.ShouldBe(Api.Url("api/foo/bar").ToString());
|
||||
}
|
||||
|
||||
private static string Base64UrlDecode(string input)
|
||||
{
|
||||
var output = input;
|
||||
output = output.Replace('-', '+').Replace('_', '/');
|
||||
|
||||
switch (output.Length % 4)
|
||||
{
|
||||
case 2: output += "=="; break;
|
||||
case 3: output += "="; break;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue