Skip to content

Commit

Permalink
updated docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Erwinvandervalk committed Jan 10, 2025
1 parent 25f3260 commit ed22ffd
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 137 deletions.
25 changes: 21 additions & 4 deletions bff/samples/Bff/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@

internal static class Extensions
{
public static WebApplication ConfigureServices(this WebApplicationBuilder builder, Func<IServiceProvider> getServiceProvider)
public static WebApplication ConfigureServices(
this WebApplicationBuilder builder,

// The serviceprovider is needed to do service discovery
Func<IServiceProvider> getServiceProvider
)
{
var services = builder.Services;

Expand Down Expand Up @@ -58,9 +63,11 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde
})
.AddOpenIdConnect("oidc", options =>
{
var resolver = getServiceProvider().GetRequiredService<ServiceEndpointResolver>();
var authority = resolver.GetEndpointsAsync("https://identity-server", CancellationToken.None).GetAwaiter().GetResult();
options.Authority = authority.Endpoints.First().ToString().TrimEnd('/');
// Normally, here you simply configure the authority. But here we want to
// use service discovery, because aspire can change the url's at run-time.
// So, it needs to be discovered at runtime.
var authority = DiscoverAuthorityByName(getServiceProvider, "identity-server");
options.Authority = authority;

// confidential client using code flow + PKCE
options.ClientId = "bff";
Expand Down Expand Up @@ -91,6 +98,16 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde

}

private static string DiscoverAuthorityByName(Func<IServiceProvider> getServiceProvider, string serviceName)
{
// Use the ServiceEndpointResolver to perform service discovery
var resolver = getServiceProvider().GetRequiredService<ServiceEndpointResolver>();
var authorityEndpoint = resolver.GetEndpointsAsync("https://" + serviceName, CancellationToken.None)
.GetAwaiter().GetResult(); // Right now I have no way to add this async.
var authority = authorityEndpoint.Endpoints[0].ToString()!.TrimEnd('/');
return authority;
}

public static WebApplication ConfigurePipeline(this WebApplication app)
{
app.UseSerilogRequestLogging();
Expand Down
45 changes: 41 additions & 4 deletions bff/samples/Hosts.Tests/TestInfra/AppHostFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@

namespace Hosts.Tests.TestInfra;

/// <summary>
/// This fixture will launch the app host, if needed.
///
/// It has 3 modes:
/// - Directly. Then the test fixture will launch an aspire test host. It will run all tests against the aspire test host.
/// In order to make this work, there were two things that I needed to overcome (see below). Service Discovery and Shared CookieContainers.
/// - With manually run aspire host.The advantage of this is that you can keep your aspire host running
/// and only iterate on your tests. This is more efficient for writing the tests.It also leaves the door open to re-using these tests to run them against a deployed in stance somewhere in the future.Downside is that you cannot debug both your tests and host at the same time because visual studio compiles them in the same location.
/// - With NCrunch. It turns out that NCrunch doesn't support building aspire projects.
/// However, I've always found that iterating over tests using ncrunch is the fastest way to get feedback.So, to make this work, I had to add a conditional compilation.
///
/// </summary>
public class AppHostFixture : IAsyncLifetime
{
private WriteTestOutput? _activeWriter = null;
Expand Down Expand Up @@ -46,7 +58,8 @@ public async Task InitializeAsync()
}

#if !DEBUG_NCRUNCH
// Arrange
// Not running in ncrunch AND no service found running.
// So, create an apphost that will be used for the duration of this testrun.
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.Hosts_AppHost>();

Expand All @@ -71,13 +84,13 @@ public async Task InitializeAsync()

await (await appHost.BuildAsync()).StartAsync();

// Wait for all the services so that their logs are mostly written.
await resourceNotificationService.WaitForResourceAsync(
"bff",
KnownResourceStates.Running
)
.WaitAsync(TimeSpan.FromSeconds(30));


await resourceNotificationService.WaitForResourceAsync(
"bff-ef",
KnownResourceStates.Running
Expand Down Expand Up @@ -134,13 +147,20 @@ public async Task DisposeAsync()
}
}

/// <summary>
/// This method builds an http client.
/// </summary>
/// <param name="clientName"></param>
/// <returns></returns>
public HttpClient CreateHttpClient(string clientName)
{
HttpMessageHandler inner;
Uri? baseAddress = null;

if (UsingAlreadyRunningInstance)
{
// An aspire host is already found (likely was started manually)
// so build a http client that directly points to this host.
var url = clientName switch
{
"bff" => "https://localhost:5002",
Expand All @@ -150,6 +170,8 @@ public HttpClient CreateHttpClient(string clientName)

inner = new SocketsHttpHandler()
{
// We need to disable cookies and follow redirects
// because we do this manually (see below).
UseCookies = false,
AllowAutoRedirect = false,
};
Expand All @@ -158,30 +180,45 @@ public HttpClient CreateHttpClient(string clientName)
else
{
#if DEBUG_NCRUNCH
// This should not be reached for NCrunch because either the servcie is already running
// or the test base has thrown a SkipException.
throw new InvalidOperationException("This should not be reached in NCrunch");
#else
// If we're here, that means that we need to create a http client that's pointing to
// aspire.
if (App == null) throw new NotSupportedException("App should not be null");
var client = App.CreateHttpClient(clientName);
baseAddress = client.BaseAddress;

// We can't directly use the HTTP Client, beause we need cookie support, but if we
// enable that the cookies get shared across multiple requests
// https://github.com/dotnet/AspNetCore.Docs/issues/15848
// By wrapping the http client, then handling all the cookies
// ourselves, we bypass this problem.
inner = new CloningHttpMessageHandler(client);
#endif
}

// Log every call that's made (including if it was part of a redirect).
var loggingHandler =
new OutboundRequestLoggingHandler(
CreateLogger<OutboundRequestLoggingHandler>()
, _ => true)
{
InnerHandler = inner
};

// Manually take care of cookies (see reason why above)
var cookieHandler = new CookieHandler(loggingHandler, new CookieContainer());

// Follow redirects when needed.
var redirectHandler = new AutoFollowRedirectHandler(CreateLogger<AutoFollowRedirectHandler>())
{
InnerHandler = cookieHandler
};


// Return a http client that follows redirects, uses cookies and logs all requests.
// For aspire, this is needed otherwise cookies are shared, but it also
// gives a much clearer debug output (each request get's logged.
return new HttpClient(redirectHandler)
{
BaseAddress = baseAddress
Expand Down
3 changes: 3 additions & 0 deletions bff/samples/Hosts.Tests/TestInfra/BffClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

namespace Hosts.Tests.TestInfra;

/// <summary>
/// Client for the BFF. All the methods that can be invoked are here.
/// </summary>
public class BffClient
{
private readonly HttpClient _client;
Expand Down
128 changes: 0 additions & 128 deletions bff/samples/IdentityServer/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@


using Duende.IdentityServer.Models;
using Duende.IdentityModel;
using Microsoft.Extensions.ServiceDiscovery;
using Duende.IdentityServer.Stores;

namespace IdentityServerHost
{
Expand Down Expand Up @@ -33,130 +30,5 @@ public static class Config
];


}
}


public class ServiceDiscoveringClientResolver(ServiceEndpointResolver resolver) : IClientStore
{
private List<Client>? _clients = null;
private SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private async Task Initialize()
{

await _semaphore.WaitAsync();
try
{


if (_clients != null)
{
return;
}
var bffUrl = (await resolver.GetEndpointsAsync("https://bff", CancellationToken.None)).Endpoints.First().EndPoint.ToString();

_clients = [
new Client
{
ClientId = "bff",
ClientSecrets = { new Secret("secret".Sha256()) },

AllowedGrantTypes =
{
GrantType.AuthorizationCode,
GrantType.ClientCredentials,
OidcConstants.GrantTypes.TokenExchange
},

RedirectUris = { $"{bffUrl}signin-oidc" },
FrontChannelLogoutUri = $"{bffUrl}signout-oidc",
PostLogoutRedirectUris = { $"{bffUrl}signout-callback-oidc" },

AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },

AccessTokenLifetime = 75 // Force refresh
},
new Client
{
ClientId = "bff.dpop",
ClientSecrets = { new Secret("secret".Sha256()) },
RequireDPoP = true,

AllowedGrantTypes =
{
GrantType.AuthorizationCode,
GrantType.ClientCredentials,
OidcConstants.GrantTypes.TokenExchange
},

RedirectUris = { "https://localhost:5003/signin-oidc" },
FrontChannelLogoutUri = "https://localhost:5003/signout-oidc",
PostLogoutRedirectUris = { "https://localhost:5003/signout-callback-oidc" },

AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },

AccessTokenLifetime = 75 // Force refresh
},
new Client
{
ClientId = "bff.ef",
ClientSecrets = { new Secret("secret".Sha256()) },

AllowedGrantTypes =
{
GrantType.AuthorizationCode,
GrantType.ClientCredentials,
OidcConstants.GrantTypes.TokenExchange
},
RedirectUris = { "https://localhost:5004/signin-oidc" },
FrontChannelLogoutUri = "https://localhost:5004/signout-oidc",
BackChannelLogoutUri = "https://localhost:5004/bff/backchannel",
PostLogoutRedirectUris = { "https://localhost:5004/signout-callback-oidc" },

AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },

AccessTokenLifetime = 75 // Force refresh
},

new Client
{
ClientId = "blazor",
ClientSecrets = { new Secret("secret".Sha256()) },

AllowedGrantTypes =
{
GrantType.AuthorizationCode,
GrantType.ClientCredentials,
OidcConstants.GrantTypes.TokenExchange
},

RedirectUris = { "https://localhost:5005/signin-oidc", "https://localhost:5105/signin-oidc" },
PostLogoutRedirectUris =
{
"https://localhost:5005/signout-callback-oidc", "https://localhost:5105/signout-callback-oidc"
},

AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },

AccessTokenLifetime = 75
}
];
}
finally
{
_semaphore.Release();
}

}


public async Task<Client> FindClientByIdAsync(string clientId)
{
await Initialize();
return _clients?.FirstOrDefault(x => x.ClientId == clientId);
}
}
2 changes: 1 addition & 1 deletion bff/samples/IdentityServer/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde
// in-memory, code config
isBuilder.AddInMemoryIdentityResources(Config.IdentityResources);
isBuilder.AddInMemoryApiScopes(Config.ApiScopes);
isBuilder.AddClientStore<ServiceDiscoveringClientResolver>();
isBuilder.AddClientStore<ServiceDiscoveringClientStore>();
isBuilder.AddInMemoryApiResources(Config.ApiResources);
isBuilder.AddExtensionGrantValidator<TokenExchangeGrantValidator>();

Expand Down
Loading

0 comments on commit ed22ffd

Please sign in to comment.