From cd63a592a5923d312f8168b3b1c282233108bb99 Mon Sep 17 00:00:00 2001 From: Axi Date: Mon, 1 Dec 2025 01:12:24 +0200 Subject: [PATCH 01/16] Add Logto with PostgreSQL and Redis integration (#815) - Introduced `CommunityToolkit.Aspire.Hosting.Logto` project for integrating Logto with PostgreSQL and Redis. - Added extension methods for configuring Logto containers, health checks, and resource dependencies. - Created test projects for validating Logto container configuration and health checks. - Added example projects under `examples/logto` showcasing Logto integration with PostgreSQL and Redis. - Updated solution file and package references to include the new Logto project. --- CommunityToolkit.Aspire.slnx | 6 + .../AppHost.cs | 14 ++ ...oolkit.Aspire.Hosting.Logto.AppHost.csproj | 20 ++ .../Properties/launchSettings.json | 31 +++ .../appsettings.json | 9 + ...spire.Hosting.Logto.ServiceDefaults.csproj | 15 ++ .../Extensions.cs | 128 ++++++++++ ...mmunityToolkit.Aspire.Hosting.Logto.csproj | 16 ++ .../LogtoBuilderExtensions.cs | 225 ++++++++++++++++++ .../LogtoContainerResource.cs | 66 +++++ .../LogtoContainerTags.cs | 16 ++ .../README.md | 69 ++++++ .../AppHostTest.cs | 30 +++ ...yToolkit.Aspire.Hosting.Logto.Tests.csproj | 8 + .../ResourceCreationTests.cs | 44 ++++ 15 files changed, 697 insertions(+) create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/Properties/launchSettings.json create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/appsettings.json create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults.csproj create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults/Extensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerTags.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Logto/README.md create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/CommunityToolkit.Aspire.Hosting.Logto.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index a66c47b9a..c8233760d 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -69,6 +69,10 @@ + + + + @@ -177,6 +181,7 @@ + @@ -230,6 +235,7 @@ + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs new file mode 100644 index 000000000..14fa70e15 --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs @@ -0,0 +1,14 @@ +using CommunityToolkit.Aspire.Hosting.Logto; + +var builder = DistributedApplication.CreateBuilder(args); + +var postgres = builder.AddPostgres("postgres") + .WithDataVolume(); + +var cache = builder.AddRedis("redis") + .WithDataVolume(); + +var logto = builder.AddLogtoContainer("logto", postgres) + .WithRedis(cache); + +builder.Build().Run(); \ No newline at end of file diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj new file mode 100644 index 000000000..02ee1c066 --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/Properties/launchSettings.json b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..7a1e3ad1e --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17139;http://localhost:15140", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21242", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23087", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22172" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15140", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19078", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18181", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20004" + } + } + } +} diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/appsettings.json b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults.csproj new file mode 100644 index 000000000..75be9da31 --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults/Extensions.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..9a2ef56a7 --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ServiceDefaults/Extensions.cs @@ -0,0 +1,128 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, + new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }); + } + + return app; + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj b/src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj new file mode 100644 index 000000000..af63e4a5e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj @@ -0,0 +1,16 @@ + + + + + .NET Aspire hosting extensions for Logto (includes PostgreSQL and Redis integration). + logto redis postgres hosting extensions + true + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs new file mode 100644 index 000000000..2eaac3ccd --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -0,0 +1,225 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using Microsoft.Extensions.DependencyInjection; + +namespace CommunityToolkit.Aspire.Hosting.Logto; + +/// +/// Provides extension methods for configuring and managing Logto container resources +/// within the Aspire hosting application framework. +/// +public static class LogtoBuilderExtensions +{ + /// + /// Adds a Logto container resource to the Aspire distributed application by configuring it + /// with the specified name, associated PostgreSQL server resource, and database name. + /// + /// The distributed application builder to which the Logto container resource will be added. + /// The name of the Logto container resource. + /// The resource builder for the PostgreSQL server that the Logto container will connect to. + /// The resource builder configured for the added Logto container resource. + public static IResourceBuilder AddLogtoContainer( + this IDistributedApplicationBuilder builder, + string name, + IResourceBuilder postgres) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + ArgumentNullException.ThrowIfNull(postgres); + + + var resource = new LogtoContainerResource(name); + var builderWithResource = builder + .AddResource(resource) + .WithImage(LogtoContainerTags.Image, LogtoContainerTags.Tag) + .WithImageRegistry(LogtoContainerTags.Registry); + + builderWithResource.WithResourcePort(); + builderWithResource.WithDatabase(postgres); + SetHealthCheck(builder, builderWithResource, name); + + + builderWithResource + .WithEntrypoint("sh") + .WithArgs( + "-c", + "npm run cli db seed -- --swe && npm start" + ); + + + return builderWithResource; + } + + + private static void SetHealthCheck(IDistributedApplicationBuilder builder, + IResourceBuilder builderWithResource, string name) + { + var endpoint = builderWithResource.Resource.GetEndpoint(LogtoContainerResource.PrimaryEndpointName); + var healthCheckKey = $"{name}_check"; + builder.Services.AddHealthChecks() + .AddUrlGroup(opt => + { + var uri = new Uri(endpoint.Url); + opt.AddUri(new Uri(uri, "/api/status"), setup => setup.ExpectHttpCode(204)); + }, healthCheckKey); + builderWithResource.WithHealthCheck(healthCheckKey); + } + + /// The resource builder for the Logto container resource to be configured. + extension(IResourceBuilder builder) + { + /// + /// Configures the Logto container resource to use the specified Node.js environment value + /// by setting the corresponding environment variable. + /// + /// The value of the Node.js environment variable to set, typically "development", "production", or "test". + /// The resource builder for the configured Logto container resource. + public IResourceBuilder WithNodeEnv(string env) + { + return builder.WithEnvironment("NODE_ENV", env); + } + + /// + /// Configures the Logto container resource with a data volume, allowing persistent storage + /// for the container. + /// + /// The optional name of the data volume. If not provided, a default name is generated. + /// The resource builder configured with the specified data volume. + public IResourceBuilder WithDataVolume(string? name = null) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/data"); + } + + + /// + /// Configures HTTP endpoints for the given Logto container resource builder with specified port settings. + /// + /// The HTTP port to be configured for the primary endpoint. Defaults to the Logto container's default HTTP port. + /// The HTTP port to be configured for the administrative endpoint. Defaults to the Logto container's default HTTP admin port. + /// The updated resource builder with the configured HTTP endpoints. + public IResourceBuilder WithResourcePort( + int port = LogtoContainerResource.DefaultHttpPort, + int adminPort = LogtoContainerResource.DefaultHttpAdminPort) + { + return builder.WithHttpEndpoint(port, port, + name: LogtoContainerResource.PrimaryEndpointName) + .WithHttpEndpoint(adminPort, adminPort, + name: LogtoContainerResource.AdminEndpointName); + } + + /// + /// Configures the specified Logto container resource to include an administrative endpoint + /// with the given URL. + /// + /// The URL of the administrative endpoint to be used for the Logto container resource. + /// The resource builder for the configured Logto container resource. + public IResourceBuilder WithAdminEndpoint(string url) + { + //example: https://admin.domain.com + return builder.WithEnvironment("ADMIN_ENDPOINT", url); + } + + /// + /// Configures the Logto container resource to disable the Admin Console port. + /// When set to true and ADMIN_ENDPOINT is unset, it will completely disable the Admin Console. + /// + /// + /// A boolean value indicating whether to disable the Admin Console port. + /// Set to true to disable the port for Admin Console; otherwise, false. + /// With ADMIN_ENDPOINT unset, setting this to true will completely disable the Admin Console. + /// + /// The resource builder for the configured Logto container resource. + public IResourceBuilder WithDisableAdminConsole(bool disable) + { + return builder.WithEnvironment("ADMIN_DISABLE_LOCALHOST", disable.ToString()); + } + + /// + /// Configures the Logto container resource to enable or disable the trust proxy header behavior + /// based on the specified value. + /// + /// + /// A boolean value indicating whether to trust the proxy header. + /// Set to true to trust the proxy header; otherwise, false. + /// + /// The resource builder for the configured Logto container resource. + public IResourceBuilder WithTrustProxyHeader(bool trustProxyHeader) + { + return builder.WithEnvironment("TRUST_PROXY_HEADER", trustProxyHeader.ToString()); + } + + /// + /// Specifies whether the username is case-sensitive. + /// + /// A value indicating whether usernames should be treated as case-sensitive. + /// The updated resource builder with the configured case-sensitivity setting. + public IResourceBuilder WithSensitiveUsername(bool sensitiveUsername) + { + return builder.WithEnvironment("CASE_SENSITIVE_USERNAME", sensitiveUsername.ToString()); + } + + /// + /// Configures the Logto container resource to use a secret vault with the specified key encryption key (KEK). + /// The KEK is used to encrypt Data Encryption Keys (DEK) in the Secret Vault and must be a base64-encoded string. + /// AES-256 (32 bytes) is recommended. Example: crypto.randomBytes(32).toString('base64') + /// + /// The base64-encoded key encryption key (KEK) for the secret vault. Must be base64-encoded; AES-256 (32 bytes) is recommended. + public IResourceBuilder WithSecretVault(string secretVaultKek) + { + return builder.WithEnvironment("SECRET_VAULT_KEK", secretVaultKek); + } + + /// + /// Configures the Logto container resource to use a data bind mount with the specified + /// source directory as the data volume for the container. + /// + /// The host directory to be mounted as the container's data volume. + /// The resource builder for the configured Logto container resource. + public IResourceBuilder WithDataBindMount(string source) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + + return builder.WithBindMount(source, "/data"); + } + + /// + /// Configures the Logto container resource to use a specified Redis resource for caching or other functionality + /// by setting the REDIS_URL environment variable and establishing a dependency on the Redis resource. + /// + /// The resource builder for the Redis resource to be used by the Logto container resource. + /// The resource builder configured with the specified Redis resource. + public IResourceBuilder WithRedis(IResourceBuilder redis) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(redis); + + return builder.WithEnvironment("REDIS_URL", redis.Resource.UriExpression) + .WaitFor(redis); + } + + /// + /// Configures the Logto container resource to connect to the specified PostgreSQL database + /// by setting the appropriate environment variables and establishing a dependency on the database resource. + /// + /// The resource builder for the PostgreSQL server to connect to. + /// The resource builder for the configured Logto container resource. + public IResourceBuilder WithDatabase(IResourceBuilder postgres) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(postgres); + + var dbUrlBuilder = new ReferenceExpressionBuilder(); + //I don't why actually db must be logto_db + dbUrlBuilder.Append($"{postgres!.Resource.UriExpression}/logto_db"); + var dbUrl = dbUrlBuilder.Build(); + + + builder.WithEnvironment("DB_URL", dbUrl) + .WaitFor(postgres); + return builder; + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerResource.cs new file mode 100644 index 000000000..efec2668b --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerResource.cs @@ -0,0 +1,66 @@ +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Logto; + +/// +/// Represents a containerized resource specific to Logto that extends the base functionality +/// of container resources by providing additional endpoint and connection string management. +/// +/// +/// This class is designed for use in an application hosting environment and incorporates +/// a primary HTTP endpoint with predefined default port configurations. +/// +public sealed class LogtoContainerResource(string name) + : ContainerResource(name), IResourceWithConnectionString +{ + internal const string PrimaryEndpointName = "http"; + internal const string AdminEndpointName = "admin"; + internal const int DefaultHttpPort = 3001; + internal const int DefaultHttpAdminPort = 3002; + + + /// Gets the primary endpoint associated with the container resource. + /// This property provides a reference to the primary HTTP endpoint for the resource, + /// facilitating network communication and identifying the primary access point. + /// The endpoint is tied to the default configuration for HTTP-based interactions + /// and is predefined with a specific protocol and port settings. + public EndpointReference PrimaryEndpoint => new(this, PrimaryEndpointName); + + /// Gets the host associated with the primary endpoint of the container resource. + /// This property allows access to the host definition of the primary HTTP endpoint, + /// which is referenced by using the `EndpointProperty.Host`. + /// The Host provides necessary information for identifying the network address + /// or location of the primary endpoint associated with this container resource. + public EndpointReferenceExpression Host => PrimaryEndpoint.Property(EndpointProperty.Host); + + /// Gets the port number associated with the primary HTTP endpoint of this resource. + /// This property represents the port component of the endpoint where the resource + /// is accessible. It is derived from the `PrimaryEndpoint` and corresponds to the + /// value of the `Port` property in the endpoint configuration. + /// The port is typically used to distinguish network services on the same host + /// and is crucial in forming a valid connection string or URL for resource access. + /// For example, this value may represent a default port for the application or + /// a specific port explicitly configured for the resource's endpoint. + /// This property is especially relevant when constructing connection strings or + /// when validation of the endpoint's configuration is required. + public EndpointReferenceExpression Port => PrimaryEndpoint.Property(EndpointProperty.Port); + + + private ReferenceExpression GetConnectionString() + { + var builder = new ReferenceExpressionBuilder(); + + builder.Append( + $"Endpoint={PrimaryEndpointName}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + + return builder.Build(); + } + + /// Gets the connection string expression for the Logto container resource. + /// The connection string is dynamically constructed based on the resource's + /// endpoint configuration and includes details such as the protocol, host, + /// and port. This property provides a reference to the connection string, + /// allowing integration with external resources or clients requiring + /// connection details formatted as a string expression. + public ReferenceExpression ConnectionStringExpression => GetConnectionString(); +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerTags.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerTags.cs new file mode 100644 index 000000000..29337a40e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerTags.cs @@ -0,0 +1,16 @@ +namespace CommunityToolkit.Aspire.Hosting.Logto; + +/// +/// Represents a collection of constants for container tags related to the Logto application. +/// +public class LogtoContainerTags +{ + /// docker.io + public const string Registry = "docker.io"; + + /// svhd/logto + public const string Image = "svhd/logto"; + + /// 1.34 + public const string Tag = "1.34"; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/README.md b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md new file mode 100644 index 000000000..bebc97886 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md @@ -0,0 +1,69 @@ +# Logto Hosting Extensions for .NET Aspire + +## Overview + +This package provides **.NET Aspire hosting extensions** for integrating **Logto** with your AppHost. +It includes helpers for wiring Logto to **PostgreSQL** (via `Aspire.Hosting.Postgres.AddPostgres()`) and optional **Redis** caching, and exposes fluent APIs to configure the required environment variables for Logto database connectivity, initialization, and caching. + +--- + +## Features + +- Configure **Logto** to use **PostgreSQL** via `AddLogtoContainer(...)`. +- Optional **Redis** integration for caching via `.WithRedis(...)`. +- Fluent helpers to set environment variables: + - `DB_URL` (Postgres connection string) + - `REDIS_URL` + - `NODE_ENV` + - `ADMIN_ENDPOINT` +- Data persistence via: + - `.WithDataVolume()` (managed Docker volume) + - `.WithDataBindMount()` (host bind mount). +- Configurable **Admin Console** access and **proxy header** trust (`TRUST_PROXY_HEADER`). +- Built-in health check for `/api/status`. + +--- + +## Usage (AppHost) + +```csharp +using Aspire.Hosting; +using Aspire.Hosting.Postgres; +using CommunityToolkit.Aspire.Hosting.Logto; + +var postgres = builder.AddPostgres("postgres"); + +// Basic setup connecting to Postgres +var logto = builder + .AddLogtoContainer("logto", postgres, databaseName: "logto_db") + .WithDataVolume(); + +// Advanced setup with Redis and specific configurations +var redis = builder.AddRedis("redis"); + +var logtoSecure = builder + .AddLogtoContainer("logto-secure", postgres, databaseName: "logto_secure_db") + .WithRedis(redis) + .WithAdminEndpoint("https://admin.example.com") + .WithDisableAdminConsole(false) + .WithTrustProxyHeader(true) // optional override, default is already true + .WithSensitiveUsername(true) + .WithNodeEnv("production"); +```` + +Logto will be configured with: + +* `DB_URL=postgresql://.../logto_db` (constructed from the Postgres resource) +* `REDIS_URL=...` (when Redis is attached with `.WithRedis(...)`) +* `ADMIN_ENDPOINT=...` (when configured with `.WithAdminEndpoint(...)`) +* `NODE_ENV=production` (when configured with `.WithNodeEnv(...)`) +* Auto-configured health checks on `/api/status`. + +--- + +## Notes + +* Extension methods are in the `CommunityToolkit.Aspire.Hosting.Logto` namespace. +* The resource automatically runs the database seeding command + `npm run cli db seed -- --swe && npm start` on startup. +* Default ports are **3001** (HTTP) and **3002** (Admin). diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs new file mode 100644 index 000000000..86e846537 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs @@ -0,0 +1,30 @@ +using Aspire.Components.Common.Tests; +using Aspire.Hosting; +using CommunityToolkit.Aspire.Testing; + +namespace CommunityToolkit.Aspire.Hosting.Logto.Tests; + +[RequiresDocker] +public class AppHostTest +{ + [Fact] + public async Task LogtoResourceStartsAndRespondsOk() + { + var builder = DistributedApplication.CreateBuilder(); + var postgres = builder.AddPostgres("postgres"); + var logto = builder.AddLogtoContainer("logto", postgres); + + using var app = builder.Build(); + var rns = app.Services.GetRequiredService(); + + await app.StartAsync(); + + // Wait for the resource to be healthy + await rns.WaitForResourceHealthyAsync(logto.Resource.Name).WaitAsync(TimeSpan.FromMinutes(5)); + + var httpClient = app.CreateHttpClient(logto.Resource.Name); + var response = await httpClient.GetAsync("/"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/CommunityToolkit.Aspire.Hosting.Logto.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/CommunityToolkit.Aspire.Hosting.Logto.Tests.csproj new file mode 100644 index 000000000..3c8568429 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/CommunityToolkit.Aspire.Hosting.Logto.Tests.csproj @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs new file mode 100644 index 000000000..1c57b5eb0 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs @@ -0,0 +1,44 @@ +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Logto.Tests; + +public class ResourceCreationTests +{ + [Fact] + public void LogtoResourceGetsAdded() + { + var builder = DistributedApplication.CreateBuilder(); + + var postgres = builder.AddPostgres("postgres"); + + builder.AddLogtoContainer("logto", postgres, "logtoDb"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("logto", resource.Name); + } + + [Fact] + public void LogtoResourceHealthChecks() + { + var builder = DistributedApplication.CreateBuilder(); + + var postgres = builder.AddPostgres("postgres"); + + builder.AddLogtoContainer("logto", postgres, "logtoDb"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + var result = resource.TryGetAnnotationsOfType(out var annotations); + Assert.True(result); + Assert.NotNull(annotations); + } +} \ No newline at end of file From 0daa9ae5b4892aa05cb7c8e0674417e1f0588cbd Mon Sep 17 00:00:00 2001 From: Axi Date: Mon, 1 Dec 2025 03:06:10 +0200 Subject: [PATCH 02/16] Add Logto client hosting support (#817) - Introduced `CommunityToolkit.Aspire.Hosting.Logto.Client` project for integrating Logto client configuration. - Added `LogtoClientBuilder` for seamless setup of Logto client services in `IHostApplicationBuilder`. - Implemented connection string helper for parsing Logto connection strings. - Updated solution and centralized package references to include the new project. --- CommunityToolkit.Aspire.slnx | 1 + Directory.Packages.props | 3 +- ...Toolkit.Aspire.Hosting.Logto.Client.csproj | 14 ++++ .../LogtoClientBuilder.cs | 76 +++++++++++++++++++ .../LogtoConnectionStringHelper.cs | 45 +++++++++++ .../ResourceCreationTests.cs | 4 +- 6 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoConnectionStringHelper.cs diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index c8233760d..20aa7846f 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -181,6 +181,7 @@ + diff --git a/Directory.Packages.props b/Directory.Packages.props index d2b01aa6f..2a31ccb6a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,6 @@ true - @@ -20,6 +19,7 @@ + @@ -115,7 +115,6 @@ - diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj new file mode 100644 index 000000000..64a133e7a --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs new file mode 100644 index 000000000..ae9466166 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs @@ -0,0 +1,76 @@ +using Logto.AspNetCore.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Logto.Client; + +/// +/// Provides methods to configure and add Logto client services to an application builder. +/// +public static class LogtoClientBuilder +{ + private const string DefaultConfigSectionName = "Aspire:Logto:Client"; + + + /// + /// Adds Logto client configuration and services to the application builder. + /// + /// + /// The used to configure the application. + /// + /// + /// The name of the connection string to retrieve the Logto endpoint from + /// (optional, default is null). + /// + /// + /// The name of the configuration section containing Logto settings + /// (optional, default is "Aspire:Logto:Client"). + /// + /// + /// An action to configure additional settings (optional). + /// + /// + /// Thrown if the configuration lacks a valid Logto Endpoint or AppId. + /// + public static void AddLogtoClient(this IHostApplicationBuilder builder, + string? connectionName = null, + string? configurationSectionName = DefaultConfigSectionName, + Action? configureSettings = null) + { + ArgumentNullException.ThrowIfNull(builder); + + var options = new LogtoOptions(); + + var sectionName = configurationSectionName ?? DefaultConfigSectionName; + builder.Configuration.GetSection(sectionName).Bind(options); + + if (!string.IsNullOrEmpty(connectionName) && + builder.Configuration.GetConnectionString(connectionName) is { } cs) + { + var endpointFromCs = LogtoConnectionStringHelper.GetEndpointFromConnectionString(cs); + + if (!string.IsNullOrWhiteSpace(endpointFromCs) && + string.IsNullOrWhiteSpace(options.Endpoint)) + { + options.Endpoint = endpointFromCs; + } + } + if (string.IsNullOrWhiteSpace(options.Endpoint)) + throw new InvalidOperationException( + $"Logto Endpoint must be configured in configuration section '{sectionName}' or via configureOptions."); + + builder.Services.AddLogtoAuthentication(opt => + { + opt.Endpoint = options.Endpoint; + opt.AppId = options.AppId; + opt.AppSecret = options.AppSecret; + configureSettings?.Invoke(opt); + + if (string.IsNullOrWhiteSpace(opt.Endpoint)) + throw new InvalidOperationException("Logto Endpoint must be configured."); + + if (string.IsNullOrEmpty(opt.AppId)) + throw new InvalidOperationException("Logto AppId must be configured."); + }); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoConnectionStringHelper.cs b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoConnectionStringHelper.cs new file mode 100644 index 000000000..d52e814b6 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoConnectionStringHelper.cs @@ -0,0 +1,45 @@ +using System.Data.Common; + +namespace CommunityToolkit.Aspire.Hosting.Logto.Client; + +/// +/// +/// +public class LogtoConnectionStringHelper +{ + private const string ConnectionStringEndpointKey = "Endpoint"; + + /// + /// Retrieves the endpoint value from a given connection string. If the connection string is a valid URI, + /// the method returns the URI as a string. If the connection string is in a key-value pair format, + /// it extracts the value of the "Endpoint" key if present and validates it as a URI. + /// + /// The connection string to parse for an endpoint. + /// + /// A string representation of the endpoint if found and valid; otherwise, null if the connection + /// string is null, empty, or does not contain a valid endpoint. + /// + public static string? GetEndpointFromConnectionString(string? connectionString) + { + if (string.IsNullOrWhiteSpace(connectionString)) + { + return null; + } + + if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + { + return uri.ToString(); + } + + var builder = new DbConnectionStringBuilder { ConnectionString = connectionString }; + + if (builder.TryGetValue(ConnectionStringEndpointKey, out var endpointObj) && + endpointObj is string endpoint && + Uri.TryCreate(endpoint, UriKind.Absolute, out var endpointUri)) + { + return endpointUri.ToString(); + } + + return null; + } +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs index 1c57b5eb0..6aef836fa 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs @@ -11,7 +11,7 @@ public void LogtoResourceGetsAdded() var postgres = builder.AddPostgres("postgres"); - builder.AddLogtoContainer("logto", postgres, "logtoDb"); + builder.AddLogtoContainer("logto", postgres); using var app = builder.Build(); @@ -29,7 +29,7 @@ public void LogtoResourceHealthChecks() var postgres = builder.AddPostgres("postgres"); - builder.AddLogtoContainer("logto", postgres, "logtoDb"); + builder.AddLogtoContainer("logto", postgres); using var app = builder.Build(); From 34b16e22074ed0f6d32127daecc72165ca3a3ab4 Mon Sep 17 00:00:00 2001 From: Axi Date: Tue, 2 Dec 2025 14:50:29 +0200 Subject: [PATCH 03/16] Add Logto Client API example project - Introduced `CommunityToolkit.Aspire.Hosting.Logto.ClientApi` under `examples/logto` to demonstrate Logto client integration. - Added project configuration files (`Program.cs`, `appsettings.json`, `launchSettings.json`) for application setup. - Renamed `AddLogtoClient` to `AddLogtoSDKClient` in `LogtoClientBuilder`. - Updated solution and centralized package references to include the new example project and dependencies. --- CommunityToolkit.Aspire.slnx | 1 + Directory.Packages.props | 2 ++ ...tyToolkit.Aspire.Hosting.Logto.Client.http | 6 +++++ ...lkit.Aspire.Hosting.Logto.ClientApi.csproj | 12 ++++++++++ .../Program.cs | 17 ++++++++++++++ .../Properties/launchSettings.json | 23 +++++++++++++++++++ .../appsettings.json | 9 ++++++++ .../LogtoClientBuilder.cs | 2 +- 8 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.Client.http create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Properties/launchSettings.json create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/appsettings.json diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index 20aa7846f..66ccdd6a0 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -71,6 +71,7 @@ + diff --git a/Directory.Packages.props b/Directory.Packages.props index 2a31ccb6a..95ca105ab 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,6 +20,8 @@ + + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.Client.http b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.Client.http new file mode 100644 index 000000000..776076030 --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.Client.http @@ -0,0 +1,6 @@ +@CommunityToolkit.Aspire.Hosting.Logto.Client_HostAddress = http://localhost:5137 + +GET {{CommunityToolkit.Aspire.Hosting.Logto.Client_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj new file mode 100644 index 000000000..16f64e80b --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj @@ -0,0 +1,12 @@ + + + CommunityToolkit.Aspire.Hosting.Logto.Client + + + + + + + + + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs new file mode 100644 index 000000000..149c55da8 --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs @@ -0,0 +1,17 @@ +using CommunityToolkit.Aspire.Hosting.Logto.Client; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddLogtoSDKClient("logto"); + +var app = builder.Build(); + + + +app.UseHttpsRedirection(); + + +app.Run(); + + +app.MapGet("/", () => "Hello World!"); \ No newline at end of file diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Properties/launchSettings.json b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Properties/launchSettings.json new file mode 100644 index 000000000..efc6b086a --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5137", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7288;http://localhost:5137", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/appsettings.json b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs index ae9466166..345d62ae5 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs @@ -32,7 +32,7 @@ public static class LogtoClientBuilder /// /// Thrown if the configuration lacks a valid Logto Endpoint or AppId. /// - public static void AddLogtoClient(this IHostApplicationBuilder builder, + public static void AddLogtoSDKClient(this IHostApplicationBuilder builder, string? connectionName = null, string? configurationSectionName = DefaultConfigSectionName, Action? configureSettings = null) From 9c725dd73230fa2c52600134be535855ca9352d4 Mon Sep 17 00:00:00 2001 From: Axi Date: Wed, 3 Dec 2025 02:37:29 +0200 Subject: [PATCH 04/16] Add tests and enhancements for Logto Client integration - Introduced a new test project `CommunityToolkit.Aspire.Hosting.Logto.Client.Tests` for validating Logto client behavior. - Added integration and unit tests for `LogtoClientBuilder` and `LogtoConnectionStringHelper`. - Implemented OIDC authentication and JWT bearer support in `LogtoClientBuilder`. - Extended `Program.cs` in `ClientApi` example with authentication routes (`/me`, `/signin`, `/signout`). - Updated dependencies and centralized package references for added functionalities. - Modified project and solution files to include updated references. --- CommunityToolkit.Aspire.slnx | 1 + Directory.Packages.props | 2 + .../AppHost.cs | 5 + ...oolkit.Aspire.Hosting.Logto.AppHost.csproj | 1 + ...lkit.Aspire.Hosting.Logto.ClientApi.csproj | 1 + .../Program.cs | 47 ++++++- ...Toolkit.Aspire.Hosting.Logto.Client.csproj | 1 + .../LogtoClientBuilder.cs | 108 ++++++++++----- .../LogtoConnectionStringHelper.cs | 4 +- ...t.Aspire.Hosting.Logto.Client.Tests.csproj | 8 ++ .../LogtoClientBuilderIntegrationTests.cs | 123 ++++++++++++++++++ .../LogtoClientBuilderTests.cs | 111 ++++++++++++++++ .../LogtoConnectionStringHelperTests.cs | 58 +++++++++ 13 files changed, 431 insertions(+), 39 deletions(-) create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index 66ccdd6a0..dc2acfdd9 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -237,6 +237,7 @@ + diff --git a/Directory.Packages.props b/Directory.Packages.props index 95ca105ab..4ebf47d0a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,9 @@ + + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs index 14fa70e15..d3be5d44e 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs @@ -11,4 +11,9 @@ var logto = builder.AddLogtoContainer("logto", postgres) .WithRedis(cache); + +var client = builder.AddProject("clientapi") + .WithReference(logto) + .WaitFor(logto); + builder.Build().Run(); \ No newline at end of file diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj index 02ee1c066..ccd7cdfd2 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj @@ -10,6 +10,7 @@ + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj index 16f64e80b..ff5658b23 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj @@ -4,6 +4,7 @@ + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs index 149c55da8..d9130a9c9 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs @@ -1,17 +1,58 @@ using CommunityToolkit.Aspire.Hosting.Logto.Client; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); +builder.AddServiceDefaults(); -builder.AddLogtoSDKClient("logto"); +builder.AddLogtoOIDC("logto", logtoOptions: config => +{ + config.AppId = "8wnwpd8smxq51ebgd5lv1"; + config.AppSecret = "SkOrdgpWNftsQlxAX7JD5gT5oospwOZ9"; +}); var app = builder.Build(); - app.UseHttpsRedirection(); app.Run(); -app.MapGet("/", () => "Hello World!"); \ No newline at end of file +app.MapGet("/", () => "Hello World!"); + +app.MapGet("/me", + [Authorize](ClaimsPrincipal user) => new + { + Name = user.Identity?.Name, + IsAuthenticated = user.Identity?.IsAuthenticated ?? false, + Claims = user.Claims.Select(c => new { c.Type, c.Value }) + }) + .WithName("Me"); + +app.MapGet("/signin", async context => +{ + if (!(context.User?.Identity?.IsAuthenticated ?? false)) + { + await context.ChallengeAsync(new AuthenticationProperties { RedirectUri = "/me" }); + } + else + { + context.Response.Redirect("/me"); + } +}); + +// Маршрут логаута +app.MapGet("/signout", async context => +{ + if (context.User?.Identity?.IsAuthenticated ?? false) + { + await context.SignOutAsync(new AuthenticationProperties { RedirectUri = "/" }); + } + else + { + context.Response.Redirect("/"); + } +}); \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj index 64a133e7a..9e06379ad 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj @@ -8,6 +8,7 @@ + diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs index 345d62ae5..4e3a09148 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs @@ -1,5 +1,9 @@ using Logto.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace CommunityToolkit.Aspire.Hosting.Logto.Client; @@ -11,41 +15,86 @@ public static class LogtoClientBuilder { private const string DefaultConfigSectionName = "Aspire:Logto:Client"; - /// - /// Adds Logto client configuration and services to the application builder. + /// Configures and adds the Logto SDK client to the specified application's service collection. /// - /// - /// The used to configure the application. - /// - /// - /// The name of the connection string to retrieve the Logto endpoint from - /// (optional, default is null). - /// - /// - /// The name of the configuration section containing Logto settings - /// (optional, default is "Aspire:Logto:Client"). - /// - /// - /// An action to configure additional settings (optional). - /// + /// The application builder that is used to configure the application. + /// The optional name of the connection string from the configuration to be used for Logto. + /// The name of the configuration section that contains Logto settings. Defaults to "Aspire:Logto:Client". + /// The name of the authentication scheme used for Logto. Defaults to "Logto". + /// The name of the cookie scheme used for Logto. Defaults to "Logto.Cookie". + /// A delegate to configure settings specific to Logto authentication. /// - /// Thrown if the configuration lacks a valid Logto Endpoint or AppId. + /// Thrown when the Logto "Endpoint" configuration is not specified or invalid. /// - public static void AddLogtoSDKClient(this IHostApplicationBuilder builder, + public static IServiceCollection AddLogtoOIDC(this IHostApplicationBuilder builder, string? connectionName = null, string? configurationSectionName = DefaultConfigSectionName, - Action? configureSettings = null) + string authenticationScheme = "Logto", + string cookieScheme = "Logto.Cookie", + Action? logtoOptions = null) { ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(logtoOptions); + + var options = GetEndpoint(builder.Configuration, configurationSectionName, connectionName); + + return builder.Services.AddLogtoAuthentication(authenticationScheme, cookieScheme, opt => + { + opt.Endpoint = options.Endpoint; + opt.AppId = options.AppId; + opt.AppSecret = options.AppSecret; + logtoOptions?.Invoke(opt); + }); + } + + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder builder, + string serviceName, + string authenticationScheme, + string? configurationSectionName = DefaultConfigSectionName, + Action? configureOptions = null) + { + + builder.Services + .AddOptions(authenticationScheme) + .Configure((jwt, configuration) => + { + var logto = GetEndpoint(configuration, configurationSectionName, serviceName); + jwt.Authority = logto.Endpoint.TrimEnd('/') + "/oidc"; + jwt.Audience = logto.AppId; + + // dev-хак: если Logto по http и мы в dev-окружении — можно ещё сюда + // добавить RequireHttpsMetadata = false через отдельный параметр, + // но окружение лучше проверять в другом экстеншене. + + configureOptions?.Invoke(jwt); + }); + builder.AddJwtBearer(authenticationScheme); + return builder; + + } + + private static LogtoOptions GetEndpoint(IConfiguration configuration, string? configurationSectionName, + string? connectionName) + { var options = new LogtoOptions(); var sectionName = configurationSectionName ?? DefaultConfigSectionName; - builder.Configuration.GetSection(sectionName).Bind(options); + configuration.GetSection(sectionName).Bind(options); if (!string.IsNullOrEmpty(connectionName) && - builder.Configuration.GetConnectionString(connectionName) is { } cs) + configuration.GetConnectionString(connectionName) is { } cs) { var endpointFromCs = LogtoConnectionStringHelper.GetEndpointFromConnectionString(cs); @@ -55,22 +104,11 @@ public static void AddLogtoSDKClient(this IHostApplicationBuilder builder, options.Endpoint = endpointFromCs; } } + if (string.IsNullOrWhiteSpace(options.Endpoint)) throw new InvalidOperationException( $"Logto Endpoint must be configured in configuration section '{sectionName}' or via configureOptions."); - - builder.Services.AddLogtoAuthentication(opt => - { - opt.Endpoint = options.Endpoint; - opt.AppId = options.AppId; - opt.AppSecret = options.AppSecret; - configureSettings?.Invoke(opt); - - if (string.IsNullOrWhiteSpace(opt.Endpoint)) - throw new InvalidOperationException("Logto Endpoint must be configured."); - - if (string.IsNullOrEmpty(opt.AppId)) - throw new InvalidOperationException("Logto AppId must be configured."); - }); + + return options; } } \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoConnectionStringHelper.cs b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoConnectionStringHelper.cs index d52e814b6..658cfc46b 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoConnectionStringHelper.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoConnectionStringHelper.cs @@ -3,7 +3,9 @@ namespace CommunityToolkit.Aspire.Hosting.Logto.Client; /// -/// +/// Provides utility methods for extracting and validating endpoint information +/// from connection strings in various formats. This helper is specifically designed +/// to assist with parsing connection strings for use with the Logto client configuration. /// public class LogtoConnectionStringHelper { diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests.csproj new file mode 100644 index 000000000..5c31fd3d6 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests.csproj @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs new file mode 100644 index 000000000..d362f2547 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs @@ -0,0 +1,123 @@ +using Logto.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace CommunityToolkit.Aspire.Hosting.Logto.Client.Tests; + +public class LogtoClientBuilderIntegrationTests +{ + private static WebApplicationBuilder CreateBuilderWithBaseConfig( + Dictionary? extraConfig = null) + { + var builder = WebApplication.CreateBuilder(); + + var config = new Dictionary + { + ["Aspire:Logto:Client:Endpoint"] = "https://logto.example.com", + ["Aspire:Logto:Client:AppId"] = "test-app-id", + ["Aspire:Logto:Client:AppSecret"] = "test-secret" + }; + + if (extraConfig is not null) + { + foreach (var kv in extraConfig) + { + config[kv.Key] = kv.Value; + } + } + + builder.Configuration.AddInMemoryCollection(config); + + return builder; + } + + [Fact] + public async Task AddLogtoSDKClient_RegistersLogtoAuthenticationScheme() + { + // Arrange + var builder = CreateBuilderWithBaseConfig(); + + // Act + builder.AddLogtoSDKClient(); + using var host = builder.Build(); + + // Assert + var schemes = host.Services.GetRequiredService(); + var scheme = await schemes.GetSchemeAsync("Logto"); + + Assert.NotNull(scheme); + Assert.Equal("Logto", scheme!.Name); + } + + [Fact] + public async Task AddLogtoSDKClient_AllowsOverrideOfAuthenticationScheme() + { + // Arrange + var builder = CreateBuilderWithBaseConfig(); + const string customScheme = "MyLogto"; + + // Act + builder.AddLogtoSDKClient(authenticationScheme: customScheme); + using var host = builder.Build(); + + // Assert + var schemes = host.Services.GetRequiredService(); + var scheme = await schemes.GetSchemeAsync(customScheme); + + Assert.NotNull(scheme); + Assert.Equal(customScheme, scheme!.Name); + } + + [Fact] + public void AddLogtoSDKClient_UsesConnectionStringEndpoint_WhenSectionEndpointMissing() + { + // Arrange: удаляем Endpoint из секции, оставляем только в connection string + var extraConfig = new Dictionary + { + ["Aspire:Logto:Client:Endpoint"] = null, + ["ConnectionStrings:Logto"] = "Endpoint=https://logto-from-cs.example.com" + }; + + var builder = CreateBuilderWithBaseConfig(extraConfig); + + // Act + builder.AddLogtoSDKClient(connectionName: "Logto"); + using var host = builder.Build(); + + // Assert: как минимум убедимся, что всё собралось + // и LogtoOptions вообще зарегистрированы (если библиотека их регистрирует) + var optionsMonitor = host.Services.GetService>(); + Assert.NotNull(optionsMonitor); + + var options = optionsMonitor!.Get("Logto"); // имя схемы + Assert.StartsWith("https://logto-from-cs.example.com", options.Endpoint); + Assert.Equal("test-app-id", options.AppId); + Assert.Equal("test-secret", options.AppSecret); + } + + [Fact] + public void AddLogtoSDKClient_ConfigureSettings_CanOverrideOptions() + { + // Arrange + var builder = CreateBuilderWithBaseConfig(); + + // Act + builder.AddLogtoSDKClient(configureSettings: opt => + { + opt.Endpoint = "https://overridden.example.com"; + opt.AppId = "overridden-app-id"; + }); + + using var host = builder.Build(); + + // Assert + var optionsMonitor = host.Services.GetService>(); + Assert.NotNull(optionsMonitor); + + var options = optionsMonitor!.Get("Logto"); + Assert.StartsWith("https://overridden.example.com", options.Endpoint); + Assert.Equal("overridden-app-id", options.AppId); + } +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderTests.cs new file mode 100644 index 000000000..0b275d1a0 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderTests.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Logto.Client.Tests; + +public class LogtoClientBuilderTests +{ + [Fact] + public void AddLogtoSDKClient_ThrowsArgumentNull_WhenBuilderIsNull() + { + IHostApplicationBuilder? builder = null; + + var ex = Assert.Throws(() => + builder!.AddLogtoSDKClient()); + + Assert.Equal("builder", ex.ParamName); + } + + [Fact] + public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenEndpointNotConfiguredAnywhere() + { + var builder = Host.CreateApplicationBuilder(); + builder.Configuration.AddInMemoryCollection(new Dictionary + { + //empty + }); + + var ex = Assert.Throws(() => + builder.AddLogtoSDKClient()); + + Assert.Contains("Logto Endpoint must be configured", ex.Message); + } + + [Fact] + public void AddLogtoSDKClient_UsesEndpointFromConfiguration_WhenPresent() + { + var builder = Host.CreateApplicationBuilder(); + + builder.Configuration.AddInMemoryCollection(new Dictionary + { + ["Aspire:Logto:Client:Endpoint"] = "https://logto-config.example.com", + ["Aspire:Logto:Client:AppId"] = "test-app-id", + ["Aspire:Logto:Client:AppSecret"] = "test-secret", + }); + + builder.AddLogtoSDKClient(); + + var host = builder.Build(); + Assert.NotNull(host); + } + + [Fact] + public void AddLogtoSDKClient_UsesEndpointFromConnectionString_WhenConfigDoesNotContainEndpoint() + { + var builder = Host.CreateApplicationBuilder(); + + builder.Configuration.AddInMemoryCollection(new Dictionary + { + ["Aspire:Logto:Client:AppId"] = "test-app-id", + ["Aspire:Logto:Client:AppSecret"] = "test-secret", + ["ConnectionStrings:Logto"] = "Endpoint=https://logto-from-cs.example.com" + }); + + builder.AddLogtoSDKClient(connectionName: "Logto"); + + var host = builder.Build(); + Assert.NotNull(host); + } + + [Fact] + public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenConfigureSettingsClearsEndpoint() + { + var builder = Host.CreateApplicationBuilder(); + + builder.Configuration.AddInMemoryCollection(new Dictionary + { + ["Aspire:Logto:Client:Endpoint"] = "https://logto-config.example.com", + ["Aspire:Logto:Client:AppId"] = "test-app-id", + ["Aspire:Logto:Client:AppSecret"] = "test-secret", + }); + + var ex = Assert.Throws(() => + builder.AddLogtoSDKClient(configureSettings: opt => + { + // Кто-то в конфиге убил Endpoint + opt.Endpoint = " "; + })); + + Assert.Equal("Logto Endpoint must be configured.", ex.Message); + } + + [Fact] + public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenAppIdIsMissingAfterConfigureSettings() + { + var builder = Host.CreateApplicationBuilder(); + + builder.Configuration.AddInMemoryCollection(new Dictionary + { + ["Aspire:Logto:Client:Endpoint"] = "https://logto-config.example.com", + ["Aspire:Logto:Client:AppSecret"] = "test-secret", + }); + + var ex = Assert.Throws(() => + builder.AddLogtoSDKClient(configureSettings: opt => + { + //empty + })); + + Assert.Equal("Logto AppId must be configured.", ex.Message); + } +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs new file mode 100644 index 000000000..c7c1bf598 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs @@ -0,0 +1,58 @@ +namespace CommunityToolkit.Aspire.Hosting.Logto.Client.Tests; + +public class LogtoConnectionStringHelperTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void GetEndpointFromConnectionString_ReturnsNull_WhenConnectionStringIsNullOrWhiteSpace(string? connectionString) + { + var result = LogtoConnectionStringHelper.GetEndpointFromConnectionString(connectionString); + + Assert.Null(result); + } + + [Fact] + public void GetEndpointFromConnectionString_ReturnsSameString_WhenItIsValidUri() + { + var connectionString = "https://logto.example.com/"; + + var result = LogtoConnectionStringHelper.GetEndpointFromConnectionString(connectionString); + + Assert.Equal(connectionString, result); + } + + [Fact] + public void GetEndpointFromConnectionString_ReturnsEndpoint_WhenEndpointKeyExistsInConnectionString() + { + var connectionString = + "Endpoint=https://logto.example.com;SomeOtherKey=SomeValue"; + + var result = LogtoConnectionStringHelper.GetEndpointFromConnectionString(connectionString); + + Assert.Equal("https://logto.example.com/", result); + } + + [Fact] + public void GetEndpointFromConnectionString_ReturnsNull_WhenEndpointIsNotValidUri() + { + var connectionString = + "Endpoint=not-a-valid-uri;SomeOtherKey=SomeValue"; + + var result = LogtoConnectionStringHelper.GetEndpointFromConnectionString(connectionString); + + Assert.Null(result); + } + + [Fact] + public void GetEndpointFromConnectionString_ReturnsNull_WhenEndpointKeyMissing() + { + var connectionString = + "Server=localhost;User Id=sa;Password=123;"; + + var result = LogtoConnectionStringHelper.GetEndpointFromConnectionString(connectionString); + + Assert.Null(result); + } +} \ No newline at end of file From 60f73e9c11525db06de5f5f09ece6d64740acf0a Mon Sep 17 00:00:00 2001 From: Axi Date: Sun, 7 Dec 2025 00:06:39 +0200 Subject: [PATCH 05/16] Refactor Logto client integration - Updated method names from `AddLogtoSDKClient` to `AddLogtoOIDC` for better alignment with OIDC usage. - Enhanced `AddLogtoOIDC` and `AddLogtoJwtBearer` methods to support additional configuration options. - Added `Microsoft.Extensions.DependencyInjection.Abstractions` package reference to support service registration. - Updated tests to reflect the method renaming and new configuration capabilities. - Extended `Program.cs` in the ClientApi example to include `UseAuthentication` and `UseAuthorization`. - Improved consistency and readability of XML documentation across updated methods. - Centralized package references for additional dependencies in `Directory.Packages.props`. --- Directory.Packages.props | 1 + ...lkit.Aspire.Hosting.Logto.ClientApi.csproj | 1 + .../Program.cs | 15 ++- .../LogtoClientBuilder.cs | 91 ++++++++++++------- .../LogtoBuilderExtensions.cs | 5 +- .../LogtoClientBuilderIntegrationTests.cs | 14 +-- .../LogtoClientBuilderTests.cs | 33 ++++--- 7 files changed, 96 insertions(+), 64 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4ebf47d0a..e8c8f5f93 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,6 +24,7 @@ + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj index ff5658b23..3d09d2c88 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj @@ -9,5 +9,6 @@ + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs index d9130a9c9..ff12f48a3 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs @@ -8,17 +8,21 @@ builder.AddLogtoOIDC("logto", logtoOptions: config => { - config.AppId = "8wnwpd8smxq51ebgd5lv1"; - config.AppSecret = "SkOrdgpWNftsQlxAX7JD5gT5oospwOZ9"; + config.AppId = "1oy1oel4jjk0vo1yzton0"; + config.AppSecret = "1vYKGbSQ2QXvyf24lJy1cUDFKjrDdNxQ"; +},oidcOptions: opt => +{ + opt.RequireHttpsMetadata = false; }); +builder.Services.AddAuthorization(); var app = builder.Build(); -app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); -app.Run(); app.MapGet("/", () => "Hello World!"); @@ -55,4 +59,5 @@ { context.Response.Redirect("/"); } -}); \ No newline at end of file +}); +app.Run(); \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs index 4e3a09148..8427e69d7 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Tokens; namespace CommunityToolkit.Aspire.Hosting.Logto.Client; @@ -15,74 +16,102 @@ public static class LogtoClientBuilder { private const string DefaultConfigSectionName = "Aspire:Logto:Client"; + /// - /// Configures and adds the Logto SDK client to the specified application's service collection. + /// Configures and adds the Logto OpenID Connect (OIDC) authentication for the specified application's service collection. /// - /// The application builder that is used to configure the application. - /// The optional name of the connection string from the configuration to be used for Logto. - /// The name of the configuration section that contains Logto settings. Defaults to "Aspire:Logto:Client". - /// The name of the authentication scheme used for Logto. Defaults to "Logto". - /// The name of the cookie scheme used for Logto. Defaults to "Logto.Cookie". - /// A delegate to configure settings specific to Logto authentication. - /// - /// Thrown when the Logto "Endpoint" configuration is not specified or invalid. - /// + /// The application builder used to configure the application's services and pipeline. + /// The name of the connection configuration to be used. If null, a default connection is used. + /// + /// The name of the configuration section that contains Logto client settings. + /// Defaults to "Aspire:Logto:Client". + /// + /// + /// The authentication scheme identifier for the Logto OIDC authentication. Defaults to "Logto". + /// + /// + /// The cookie scheme name to be used with the Logto OIDC authentication. Defaults to "Logto.Cookie". + /// + /// + /// A delegate to configure Logto-specific options such as endpoint, application ID, or secret. + /// + /// + /// A delegate to configure OpenID Connect options for fine-tuning authentication behavior. + /// + /// + /// An updated instance with the configured Logto OIDC authentication. + /// + /// Thrown when the builder is null. public static IServiceCollection AddLogtoOIDC(this IHostApplicationBuilder builder, string? connectionName = null, string? configurationSectionName = DefaultConfigSectionName, string authenticationScheme = "Logto", string cookieScheme = "Logto.Cookie", - Action? logtoOptions = null) + Action? logtoOptions = null, + Action? oidcOptions = null) { ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(logtoOptions); var options = GetEndpoint(builder.Configuration, configurationSectionName, connectionName); - - return builder.Services.AddLogtoAuthentication(authenticationScheme, cookieScheme, opt => + + builder.Services.AddLogtoAuthentication(authenticationScheme, cookieScheme, opt => { opt.Endpoint = options.Endpoint; opt.AppId = options.AppId; opt.AppSecret = options.AppSecret; logtoOptions?.Invoke(opt); }); + builder.Services.Configure(authenticationScheme, opt => + { + oidcOptions?.Invoke(opt); + }); + return builder.Services; } /// - /// + /// Configures and adds the Logto JWT Bearer authentication for the specified application's service collection. /// - /// - /// - /// - /// - /// - /// + /// The authentication builder that is used to configure authentication services. + /// The name of the service to be used for identifying the current Logto endpoint configuration. + /// The application ID assigned to the Logto client. + /// + /// The authentication scheme used for JWT Bearer authentication. Defaults to "Bearer". + /// + /// + /// The name of the configuration section that contains Logto settings. Defaults to "Aspire:Logto:Client". + /// + /// A delegate to further configure the JwtBearerOptions. + /// + /// An updated instance with the configured Logto JWT Bearer authentication. + /// + /// + /// Thrown when the Logto endpoint configuration is missing or invalid. + /// public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder builder, string serviceName, - string authenticationScheme, + string appId, + string authenticationScheme = JwtBearerDefaults.AuthenticationScheme, string? configurationSectionName = DefaultConfigSectionName, Action? configureOptions = null) { - builder.Services .AddOptions(authenticationScheme) .Configure((jwt, configuration) => { var logto = GetEndpoint(configuration, configurationSectionName, serviceName); - jwt.Authority = logto.Endpoint.TrimEnd('/') + "/oidc"; - jwt.Audience = logto.AppId; - - // dev-хак: если Logto по http и мы в dev-окружении — можно ещё сюда - // добавить RequireHttpsMetadata = false через отдельный параметр, - // но окружение лучше проверять в другом экстеншене. + var issuer = logto.Endpoint.TrimEnd('/') + "/oidc"; + jwt.Authority = issuer; + jwt.TokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = issuer, ValidateIssuer = true, ValidAudience = appId, ValidateAudience = true + }; configureOptions?.Invoke(jwt); }); builder.AddJwtBearer(authenticationScheme); return builder; - } private static LogtoOptions GetEndpoint(IConfiguration configuration, string? configurationSectionName, @@ -108,7 +137,7 @@ private static LogtoOptions GetEndpoint(IConfiguration configuration, string? co if (string.IsNullOrWhiteSpace(options.Endpoint)) throw new InvalidOperationException( $"Logto Endpoint must be configured in configuration section '{sectionName}' or via configureOptions."); - + return options; } } \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index 2eaac3ccd..d35f0b14f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -41,10 +41,7 @@ public static IResourceBuilder AddLogtoContainer( builderWithResource .WithEntrypoint("sh") - .WithArgs( - "-c", - "npm run cli db seed -- --swe && npm start" - ); + .WithArgs("-c", "npm run cli db seed -- --swe && npm start"); return builderWithResource; diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs index d362f2547..525e210aa 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs @@ -34,13 +34,13 @@ private static WebApplicationBuilder CreateBuilderWithBaseConfig( } [Fact] - public async Task AddLogtoSDKClient_RegistersLogtoAuthenticationScheme() + public async Task AddLogtoOIDC_RegistersLogtoAuthenticationScheme() { // Arrange var builder = CreateBuilderWithBaseConfig(); // Act - builder.AddLogtoSDKClient(); + builder.AddLogtoOIDC(); using var host = builder.Build(); // Assert @@ -52,14 +52,14 @@ public async Task AddLogtoSDKClient_RegistersLogtoAuthenticationScheme() } [Fact] - public async Task AddLogtoSDKClient_AllowsOverrideOfAuthenticationScheme() + public async Task AddLogtoOIDC_AllowsOverrideOfAuthenticationScheme() { // Arrange var builder = CreateBuilderWithBaseConfig(); const string customScheme = "MyLogto"; // Act - builder.AddLogtoSDKClient(authenticationScheme: customScheme); + builder.AddLogtoOIDC(authenticationScheme: customScheme); using var host = builder.Build(); // Assert @@ -83,7 +83,7 @@ public void AddLogtoSDKClient_UsesConnectionStringEndpoint_WhenSectionEndpointMi var builder = CreateBuilderWithBaseConfig(extraConfig); // Act - builder.AddLogtoSDKClient(connectionName: "Logto"); + builder.AddLogtoOIDC(connectionName: "Logto"); using var host = builder.Build(); // Assert: как минимум убедимся, что всё собралось @@ -98,13 +98,13 @@ public void AddLogtoSDKClient_UsesConnectionStringEndpoint_WhenSectionEndpointMi } [Fact] - public void AddLogtoSDKClient_ConfigureSettings_CanOverrideOptions() + public void AddLogtoClient_ConfigureSettings_CanOverrideOptions() { // Arrange var builder = CreateBuilderWithBaseConfig(); // Act - builder.AddLogtoSDKClient(configureSettings: opt => + builder.AddLogtoOIDC(logtoOptions: opt => { opt.Endpoint = "https://overridden.example.com"; opt.AppId = "overridden-app-id"; diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderTests.cs index 0b275d1a0..1edafa83f 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderTests.cs @@ -9,10 +9,10 @@ public class LogtoClientBuilderTests public void AddLogtoSDKClient_ThrowsArgumentNull_WhenBuilderIsNull() { IHostApplicationBuilder? builder = null; - + var ex = Assert.Throws(() => - builder!.AddLogtoSDKClient()); - + builder!.AddLogtoOIDC()); + Assert.Equal("builder", ex.ParamName); } @@ -24,9 +24,9 @@ public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenEndpointNotConfiguredAn { //empty }); - + var ex = Assert.Throws(() => - builder.AddLogtoSDKClient()); + builder.AddLogtoOIDC()); Assert.Contains("Logto Endpoint must be configured", ex.Message); } @@ -42,9 +42,9 @@ public void AddLogtoSDKClient_UsesEndpointFromConfiguration_WhenPresent() ["Aspire:Logto:Client:AppId"] = "test-app-id", ["Aspire:Logto:Client:AppSecret"] = "test-secret", }); - - builder.AddLogtoSDKClient(); - + + builder.AddLogtoOIDC(); + var host = builder.Build(); Assert.NotNull(host); } @@ -60,9 +60,9 @@ public void AddLogtoSDKClient_UsesEndpointFromConnectionString_WhenConfigDoesNot ["Aspire:Logto:Client:AppSecret"] = "test-secret", ["ConnectionStrings:Logto"] = "Endpoint=https://logto-from-cs.example.com" }); - - builder.AddLogtoSDKClient(connectionName: "Logto"); - + + builder.AddLogtoOIDC(connectionName: "Logto"); + var host = builder.Build(); Assert.NotNull(host); } @@ -78,14 +78,13 @@ public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenConfigureSettingsClears ["Aspire:Logto:Client:AppId"] = "test-app-id", ["Aspire:Logto:Client:AppSecret"] = "test-secret", }); - + var ex = Assert.Throws(() => - builder.AddLogtoSDKClient(configureSettings: opt => + builder.AddLogtoOIDC(logtoOptions: opt => { - // Кто-то в конфиге убил Endpoint opt.Endpoint = " "; })); - + Assert.Equal("Logto Endpoint must be configured.", ex.Message); } @@ -99,9 +98,9 @@ public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenAppIdIsMissingAfterConf ["Aspire:Logto:Client:Endpoint"] = "https://logto-config.example.com", ["Aspire:Logto:Client:AppSecret"] = "test-secret", }); - + var ex = Assert.Throws(() => - builder.AddLogtoSDKClient(configureSettings: opt => + builder.AddLogtoOIDC(logtoOptions: _ => { //empty })); From 8a0f9de64ad7c437c6b4cb2210435c5a619bb3fc Mon Sep 17 00:00:00 2001 From: Axi Date: Sun, 7 Dec 2025 00:06:55 +0200 Subject: [PATCH 06/16] Remove unused `/signout` route from ClientApi example --- .../CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs index ff12f48a3..61c12ca15 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs @@ -48,7 +48,6 @@ } }); -// Маршрут логаута app.MapGet("/signout", async context => { if (context.User?.Identity?.IsAuthenticated ?? false) From 95d01da784d564d3d655396842021cd53ff0646c Mon Sep 17 00:00:00 2001 From: Axi Date: Sun, 7 Dec 2025 13:01:31 +0200 Subject: [PATCH 07/16] Rename `ClientApi` example to `ClientOIDC` and update method signatures - Changed the `ClientApi` project to `ClientOIDC` for better alignment with OIDC standards. - Updated method signatures in `LogtoClientBuilder` to use `appIndeficator` instead of `appId` and support multiple audience identifiers. - Improved XML documentation consistency for updated methods. - Adjusted solution, project references, and configuration files to reflect the renaming and API changes. --- CommunityToolkit.Aspire.slnx | 2 +- .../AppHost.cs | 2 +- ...oolkit.Aspire.Hosting.Logto.AppHost.csproj | 2 +- ...tyToolkit.Aspire.Hosting.Logto.Client.http | 0 ...it.Aspire.Hosting.Logto.ClientOIDC.csproj} | 0 .../Program.cs | 0 .../Properties/launchSettings.json | 0 .../appsettings.json | 0 .../LogtoClientBuilder.cs | 58 ++++++++++++------- 9 files changed, 39 insertions(+), 25 deletions(-) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientApi => CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC}/CommunityToolkit.Aspire.Hosting.Logto.Client.http (100%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj => CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj} (100%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientApi => CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC}/Program.cs (100%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientApi => CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC}/Properties/launchSettings.json (100%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientApi => CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC}/appsettings.json (100%) diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index dc2acfdd9..a8bf03229 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -71,7 +71,7 @@ - + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs index d3be5d44e..67d1a098b 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs @@ -12,7 +12,7 @@ .WithRedis(cache); -var client = builder.AddProject("clientapi") +var client = builder.AddProject("clientapi") .WithReference(logto) .WaitFor(logto); diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj index ccd7cdfd2..1c8ff59e1 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj @@ -10,7 +10,7 @@ - + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.Client.http b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.Client.http similarity index 100% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.Client.http rename to examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.Client.http diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj similarity index 100% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/CommunityToolkit.Aspire.Hosting.Logto.ClientApi.csproj rename to examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Program.cs similarity index 100% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Program.cs rename to examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Program.cs diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Properties/launchSettings.json b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Properties/launchSettings.json similarity index 100% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/Properties/launchSettings.json rename to examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Properties/launchSettings.json diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/appsettings.json b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/appsettings.json similarity index 100% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientApi/appsettings.json rename to examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/appsettings.json diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs index 8427e69d7..033531cd4 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs @@ -68,29 +68,41 @@ public static IServiceCollection AddLogtoOIDC(this IHostApplicationBuilder build return builder.Services; } + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder builder, + string serviceName, + string appIndeficator, + string authenticationScheme = JwtBearerDefaults.AuthenticationScheme, + string? configurationSectionName = DefaultConfigSectionName, + Action? configureOptions = null) + { + return AddLogtoJwtBearer(builder, serviceName, [appIndeficator], authenticationScheme, + configurationSectionName, configureOptions); + } + /// - /// Configures and adds the Logto JWT Bearer authentication for the specified application's service collection. + /// /// - /// The authentication builder that is used to configure authentication services. - /// The name of the service to be used for identifying the current Logto endpoint configuration. - /// The application ID assigned to the Logto client. - /// - /// The authentication scheme used for JWT Bearer authentication. Defaults to "Bearer". - /// - /// - /// The name of the configuration section that contains Logto settings. Defaults to "Aspire:Logto:Client". - /// - /// A delegate to further configure the JwtBearerOptions. - /// - /// An updated instance with the configured Logto JWT Bearer authentication. - /// - /// - /// Thrown when the Logto endpoint configuration is missing or invalid. - /// + /// + /// + /// + /// + /// + /// + /// public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder builder, string serviceName, - string appId, + IEnumerable appIndeficator, string authenticationScheme = JwtBearerDefaults.AuthenticationScheme, string? configurationSectionName = DefaultConfigSectionName, Action? configureOptions = null) @@ -100,14 +112,16 @@ public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder .Configure((jwt, configuration) => { var logto = GetEndpoint(configuration, configurationSectionName, serviceName); - var issuer = logto.Endpoint.TrimEnd('/') + "/oidc"; + jwt.Authority = issuer; - jwt.TokenValidationParameters = new TokenValidationParameters + jwt.TokenValidationParameters = new TokenValidationParameters() { - ValidIssuer = issuer, ValidateIssuer = true, ValidAudience = appId, ValidateAudience = true + ValidateIssuer = true, + ValidIssuer = issuer, + ValidateAudience = true, + ValidAudiences = appIndeficator }; - configureOptions?.Invoke(jwt); }); builder.AddJwtBearer(authenticationScheme); From 541111c3dbf7262bfc338e4a0d4d10cbe3a73c25 Mon Sep 17 00:00:00 2001 From: Axi Date: Sun, 7 Dec 2025 14:03:56 +0200 Subject: [PATCH 08/16] Add `ClientJWT` example with Logto JWT authentication support - Introduced `CommunityToolkit.Aspire.Hosting.Logto.ClientJWT` project under `examples/logto` to demonstrate Logto JWT authentication. - Configured authentication and authorization middleware with Logto's JWT Bearer scheme in `Program.cs`. - Added example routes (`/secure` and `/tokens`) for testing secured endpoint access and token retrieval. - Updated `AppHost` to include `ClientJWT` project as a dependency. - Improved XML documentation for `AddLogtoJwtBearer` methods, including updated parameter descriptions and exception handling. --- CommunityToolkit.Aspire.slnx | 1 + .../AppHost.cs | 6 +- ...oolkit.Aspire.Hosting.Logto.AppHost.csproj | 1 + ...lkit.Aspire.Hosting.Logto.ClientJWT.csproj | 13 ++++ ...oolkit.Aspire.Hosting.Logto.ClientJWT.http | 6 ++ .../Program.cs | 34 ++++++++++ .../Properties/launchSettings.json | 23 +++++++ .../appsettings.json | 9 +++ .../Program.cs | 18 ++++- .../LogtoClientBuilder.cs | 67 +++++++++++++------ 10 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Program.cs create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Properties/launchSettings.json create mode 100644 examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/appsettings.json diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index a8bf03229..1fe0585d4 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -71,6 +71,7 @@ + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs index 67d1a098b..577ed0fc9 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs @@ -12,8 +12,12 @@ .WithRedis(cache); -var client = builder.AddProject("clientapi") +var clientOIDC = builder.AddProject("clientOIDC") .WithReference(logto) .WaitFor(logto); +var clientJWT = builder.AddProject("clientJWT") + .WithReference(logto) + .WaitFor(logto); + builder.Build().Run(); \ No newline at end of file diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj index 1c8ff59e1..55692d045 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj @@ -10,6 +10,7 @@ + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj new file mode 100644 index 000000000..17ca17ced --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http new file mode 100644 index 000000000..a4e1a7dc0 --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http @@ -0,0 +1,6 @@ +@CommunityToolkit.Aspire.Hosting.Logto.ClientJWT_HostAddress = http://localhost:5072 + +GET {{CommunityToolkit.Aspire.Hosting.Logto.ClientJWT_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Program.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Program.cs new file mode 100644 index 000000000..18595318f --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Program.cs @@ -0,0 +1,34 @@ +using CommunityToolkit.Aspire.Hosting.Logto.Client; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; + +var builder = WebApplication.CreateBuilder(args); +builder.AddServiceDefaults(); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddLogtoJwtBearer("logto", "http://localhost:5072/", + configureOptions: opt => + { + opt.RequireHttpsMetadata = false; + }); + +builder.Services.AddAuthorization(); + + +var app = builder.Build(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGet("/", () => "OK"); + +app.MapGet("/secure", [Authorize] (System.Security.Claims.ClaimsPrincipal user) => +{ + return new + { + user.Identity?.Name, + Claims = user.Claims.Select(c => new { c.Type, c.Value }) + }; +}); + + +app.Run(); \ No newline at end of file diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Properties/launchSettings.json b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Properties/launchSettings.json new file mode 100644 index 000000000..59456df8e --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5072", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7138;http://localhost:5072", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/appsettings.json b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Program.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Program.cs index 61c12ca15..50b3b6c70 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Program.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Program.cs @@ -8,8 +8,9 @@ builder.AddLogtoOIDC("logto", logtoOptions: config => { - config.AppId = "1oy1oel4jjk0vo1yzton0"; - config.AppSecret = "1vYKGbSQ2QXvyf24lJy1cUDFKjrDdNxQ"; + config.AppId = "s6zda5bqn1qlsjzaiklqn"; + config.AppSecret = "Df77aDt13MG3nSTgo8eKZP2HdeSfbed0"; + },oidcOptions: opt => { opt.RequireHttpsMetadata = false; @@ -47,6 +48,19 @@ context.Response.Redirect("/me"); } }); +app.MapGet("/tokens", [Authorize] async (HttpContext ctx) => +{ + var accessToken = await ctx.GetTokenAsync("access_token"); + var idToken = await ctx.GetTokenAsync("id_token"); + var refreshToken = await ctx.GetTokenAsync("refresh_token"); + + return Results.Ok(new + { + access_token = accessToken, + id_token = idToken, + refresh_token = refreshToken + }); +}); app.MapGet("/signout", async context => { diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs index 033531cd4..30b192133 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs @@ -69,40 +69,67 @@ public static IServiceCollection AddLogtoOIDC(this IHostApplicationBuilder build } /// - /// + /// Configures and adds the Logto JSON Web Token (JWT) Bearer authentication to the specified authentication builder. /// - /// - /// - /// - /// - /// - /// - /// + /// The authentication builder used to configure authentication services. + /// The name of the Logto service instance to be used for authentication. + /// A collection of application identifiers associated with the Logto service. + /// + /// The authentication scheme identifier for the Logto JWT Bearer authentication. Defaults to "Bearer". + /// + /// + /// The name of the configuration section that contains Logto client settings. Defaults to "Aspire:Logto:Client". + /// + /// + /// A delegate to configure the options for JWT bearer authentication. + /// + /// + /// An updated instance with the configured Logto JWT Bearer authentication. + /// + /// Thrown when the builder or serviceName is null. public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder builder, string serviceName, - string appIndeficator, + string appIdentification, string authenticationScheme = JwtBearerDefaults.AuthenticationScheme, string? configurationSectionName = DefaultConfigSectionName, Action? configureOptions = null) { - return AddLogtoJwtBearer(builder, serviceName, [appIndeficator], authenticationScheme, + return AddLogtoJwtBearer(builder, serviceName, [appIdentification], authenticationScheme, configurationSectionName, configureOptions); } /// - /// + /// Configures and adds the Logto JSON Web Token (JWT) Bearer authentication for the specified application. /// - /// - /// - /// - /// - /// - /// - /// + /// + /// The authentication builder used to configure the application's authentication services. + /// + /// + /// The name of the service associated with Logto configuration. + /// + /// + /// A collection of application identifiers (audiences) used to validate the JWT's audience claim. + /// + /// + /// The authentication scheme identifier for the Logto JWT Bearer authentication. Defaults to "Bearer". + /// + /// + /// The name of the configuration section that contains Logto client settings. Defaults to "Aspire:Logto:Client". + /// + /// + /// A delegate to configure additional JwtBearerOptions as needed for specific authentication behavior. + /// + /// + /// An updated instance with the configured Logto JWT Bearer authentication. + /// + /// Thrown when the builder is null. + /// + /// Thrown when serviceName or appIndeficator is missing or invalid. + /// public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder builder, string serviceName, - IEnumerable appIndeficator, + IEnumerable appIdentification, string authenticationScheme = JwtBearerDefaults.AuthenticationScheme, string? configurationSectionName = DefaultConfigSectionName, Action? configureOptions = null) @@ -120,7 +147,7 @@ public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder ValidateIssuer = true, ValidIssuer = issuer, ValidateAudience = true, - ValidAudiences = appIndeficator + ValidAudiences = appIdentification }; configureOptions?.Invoke(jwt); }); From d4378b38fbc6221638566413043db2cb4674b7df Mon Sep 17 00:00:00 2001 From: Axi Date: Mon, 30 Mar 2026 14:29:37 +0300 Subject: [PATCH 09/16] Update examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj Co-authored-by: Aaron Powell --- .../CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj index 55692d045..3a120204f 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj @@ -2,7 +2,6 @@ Exe - net10.0 enable enable From 4557bf8b139190abd2a1675ee9f7fe1ecf8936ca Mon Sep 17 00:00:00 2001 From: Axi Date: Mon, 30 Mar 2026 14:30:11 +0300 Subject: [PATCH 10/16] Update src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj Co-authored-by: Aaron Powell --- .../CommunityToolkit.Aspire.Hosting.Logto.Client.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj index 9e06379ad..5f26842b1 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj @@ -1,10 +1,5 @@  - - net10.0 - enable - enable - From 3976aec487c3d08d36365cd5ae3068a90b71cbcb Mon Sep 17 00:00:00 2001 From: Axi Date: Mon, 30 Mar 2026 16:04:56 +0300 Subject: [PATCH 11/16] Remove unused package references and resolve merge conflict in project and package configuration files --- Directory.Packages.props | 9 +-------- ...ommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj | 3 +-- ...mmunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj | 4 +--- .../CommunityToolkit.Aspire.Hosting.Logto.Client.csproj | 2 +- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 936451934..3b3ded75f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,16 +20,9 @@ -<<<<<<< Logto - - - - - -======= ->>>>>>> main + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj index 17ca17ced..9526b76d6 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj @@ -1,10 +1,9 @@ - + - diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj index 3d09d2c88..5e09bc2f6 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj @@ -7,8 +7,6 @@ - - - + diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj index 5f26842b1..bc1032e1b 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj @@ -3,8 +3,8 @@ - + From 6685b60aec3aa0b3fc9a718153a554cb281f0191 Mon Sep 17 00:00:00 2001 From: Axi Date: Mon, 30 Mar 2026 17:13:34 +0300 Subject: [PATCH 12/16] Update package versions and project SDKs for Logto client integration - Upgraded `Aspire.AppHost.Sdk` from `13.0.0` to `13.2.0` in `examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj`. - Added `Microsoft.AspNetCore.Authentication.JwtBearer` and `Microsoft.AspNetCore.Authentication.OpenIdConnect` package versions to `Directory.Packages.props`. --- Directory.Packages.props | 2 ++ .../CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj | 2 +- .../CommunityToolkit.Aspire.Hosting.Logto.Client.csproj | 3 --- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3b3ded75f..da945c68c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -34,6 +34,8 @@ + + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj index 3a120204f..96196b6be 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj index bc1032e1b..431101722 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj @@ -1,10 +1,7 @@  - - - From cde0cbd28eecc9daedad548e262f0756e8799488 Mon Sep 17 00:00:00 2001 From: Axi Date: Mon, 30 Mar 2026 17:34:11 +0300 Subject: [PATCH 13/16] Update `ClientJWT` example to use a constant for API audience in Logto JWT configuration - Replaced hardcoded API audience with a `const` string in `Program.cs` for improved readability and maintainability. --- .../Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Program.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Program.cs index 18595318f..7f1d7c90e 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Program.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Program.cs @@ -4,9 +4,9 @@ var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); - +const string apiAudience = "http://localhost:5072/"; builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddLogtoJwtBearer("logto", "http://localhost:5072/", + .AddLogtoJwtBearer("logto", appIdentification: apiAudience, configureOptions: opt => { opt.RequireHttpsMetadata = false; From d271f0a8f5aa99e71310ba4aaca5895589034df3 Mon Sep 17 00:00:00 2001 From: Axi Date: Mon, 30 Mar 2026 17:42:09 +0300 Subject: [PATCH 14/16] Remove `` property from Logto project --- .../CommunityToolkit.Aspire.Hosting.Logto.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj b/src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj index af63e4a5e..a4e35450f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj @@ -4,7 +4,6 @@ .NET Aspire hosting extensions for Logto (includes PostgreSQL and Redis integration). logto redis postgres hosting extensions - true From f282231de0b456c2244b3ea5f99f17e7bcf4aa45 Mon Sep 17 00:00:00 2001 From: Axi Date: Tue, 31 Mar 2026 10:26:17 +0300 Subject: [PATCH 15/16] Rename `Hosting.Logto.Client` to `Logto.Client` and adjust related references - Renamed `CommunityToolkit.Aspire.Hosting.Logto.Client` to `CommunityToolkit.Aspire.Logto.Client` for improved namespace consistency. - Updated all project, namespace, and solution references to reflect the renaming. - Adjusted example projects (`ClientJWT` and `ClientOIDC`) and `AppHost` references accordingly. --- CommunityToolkit.Aspire.slnx | 8 ++++---- .../AppHost.cs | 4 ++-- .../CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj | 4 ++-- .../CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http | 0 .../CommunityToolkit.Aspire.Logto.ClientJWT.csproj} | 2 +- .../Program.cs | 2 +- .../Properties/launchSettings.json | 0 .../appsettings.json | 0 .../CommunityToolkit.Aspire.Hosting.Logto.Client.http | 0 .../CommunityToolkit.Aspire.Logto.ClientOIDC.csproj} | 2 +- .../Program.cs | 2 +- .../Properties/launchSettings.json | 0 .../appsettings.json | 0 .../CommunityToolkit.Aspire.Logto.Client.csproj} | 0 .../LogtoClientBuilder.cs | 2 +- .../LogtoConnectionStringHelper.cs | 2 +- .../CommunityToolkit.Aspire.Logto.Client.Tests.csproj} | 2 +- .../LogtoClientBuilderIntegrationTests.cs | 2 +- .../LogtoClientBuilderTests.cs | 2 +- .../LogtoConnectionStringHelperTests.cs | 2 +- 20 files changed, 18 insertions(+), 18 deletions(-) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientJWT => CommunityToolkit.Aspire.Logto.ClientJWT}/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http (100%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj => CommunityToolkit.Aspire.Logto.ClientJWT/CommunityToolkit.Aspire.Logto.ClientJWT.csproj} (83%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientJWT => CommunityToolkit.Aspire.Logto.ClientJWT}/Program.cs (94%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientJWT => CommunityToolkit.Aspire.Logto.ClientJWT}/Properties/launchSettings.json (100%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientJWT => CommunityToolkit.Aspire.Logto.ClientJWT}/appsettings.json (100%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC => CommunityToolkit.Aspire.Logto.ClientOIDC}/CommunityToolkit.Aspire.Hosting.Logto.Client.http (100%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj => CommunityToolkit.Aspire.Logto.ClientOIDC/CommunityToolkit.Aspire.Logto.ClientOIDC.csproj} (86%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC => CommunityToolkit.Aspire.Logto.ClientOIDC}/Program.cs (97%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC => CommunityToolkit.Aspire.Logto.ClientOIDC}/Properties/launchSettings.json (100%) rename examples/logto/{CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC => CommunityToolkit.Aspire.Logto.ClientOIDC}/appsettings.json (100%) rename src/{CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj => CommunityToolkit.Aspire.Logto.Client/CommunityToolkit.Aspire.Logto.Client.csproj} (100%) rename src/{CommunityToolkit.Aspire.Hosting.Logto.Client => CommunityToolkit.Aspire.Logto.Client}/LogtoClientBuilder.cs (99%) rename src/{CommunityToolkit.Aspire.Hosting.Logto.Client => CommunityToolkit.Aspire.Logto.Client}/LogtoConnectionStringHelper.cs (96%) rename tests/{CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests.csproj => CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj} (65%) rename tests/{CommunityToolkit.Aspire.Hosting.Logto.Client.Tests => CommunityToolkit.Aspire.Logto.Client.Tests}/LogtoClientBuilderIntegrationTests.cs (98%) rename tests/{CommunityToolkit.Aspire.Hosting.Logto.Client.Tests => CommunityToolkit.Aspire.Logto.Client.Tests}/LogtoClientBuilderTests.cs (98%) rename tests/{CommunityToolkit.Aspire.Hosting.Logto.Client.Tests => CommunityToolkit.Aspire.Logto.Client.Tests}/LogtoConnectionStringHelperTests.cs (96%) diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index b6b45af87..47b581b6d 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -82,9 +82,9 @@ - - + + @@ -220,7 +220,6 @@ - @@ -247,6 +246,7 @@ + @@ -284,7 +284,6 @@ - @@ -310,6 +309,7 @@ + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs index 577ed0fc9..d87f5048e 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs @@ -12,10 +12,10 @@ .WithRedis(cache); -var clientOIDC = builder.AddProject("clientOIDC") +var clientOIDC = builder.AddProject("clientOIDC") .WithReference(logto) .WaitFor(logto); -var clientJWT = builder.AddProject("clientJWT") +var clientJWT = builder.AddProject("clientJWT") .WithReference(logto) .WaitFor(logto); diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj index 96196b6be..832a2091a 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http similarity index 100% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http rename to examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/CommunityToolkit.Aspire.Logto.ClientJWT.csproj similarity index 83% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj rename to examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/CommunityToolkit.Aspire.Logto.ClientJWT.csproj index 9526b76d6..a0ef7dc03 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/CommunityToolkit.Aspire.Logto.ClientJWT.csproj @@ -5,7 +5,7 @@ - + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Program.cs b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Program.cs similarity index 94% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Program.cs rename to examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Program.cs index 7f1d7c90e..db14f4123 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Program.cs +++ b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Program.cs @@ -1,4 +1,4 @@ -using CommunityToolkit.Aspire.Hosting.Logto.Client; +using CommunityToolkit.Aspire.Logto.Client; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Properties/launchSettings.json b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Properties/launchSettings.json similarity index 100% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/Properties/launchSettings.json rename to examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Properties/launchSettings.json diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/appsettings.json b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/appsettings.json similarity index 100% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT/appsettings.json rename to examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/appsettings.json diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.Client.http b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.Client.http similarity index 100% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.Client.http rename to examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.Client.http diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/CommunityToolkit.Aspire.Logto.ClientOIDC.csproj similarity index 86% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj rename to examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/CommunityToolkit.Aspire.Logto.ClientOIDC.csproj index 5e09bc2f6..44f15918a 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC.csproj +++ b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/CommunityToolkit.Aspire.Logto.ClientOIDC.csproj @@ -3,7 +3,7 @@ CommunityToolkit.Aspire.Hosting.Logto.Client - + diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Program.cs b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/Program.cs similarity index 97% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Program.cs rename to examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/Program.cs index 50b3b6c70..2afda04aa 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Program.cs +++ b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/Program.cs @@ -1,4 +1,4 @@ -using CommunityToolkit.Aspire.Hosting.Logto.Client; +using CommunityToolkit.Aspire.Logto.Client; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using System.Security.Claims; diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Properties/launchSettings.json b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/Properties/launchSettings.json similarity index 100% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/Properties/launchSettings.json rename to examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/Properties/launchSettings.json diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/appsettings.json b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/appsettings.json similarity index 100% rename from examples/logto/CommunityToolkit.Aspire.Hosting.Logto.ClientOIDC/appsettings.json rename to examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/appsettings.json diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj b/src/CommunityToolkit.Aspire.Logto.Client/CommunityToolkit.Aspire.Logto.Client.csproj similarity index 100% rename from src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityToolkit.Aspire.Hosting.Logto.Client.csproj rename to src/CommunityToolkit.Aspire.Logto.Client/CommunityToolkit.Aspire.Logto.Client.csproj diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs b/src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs similarity index 99% rename from src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs rename to src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs index 30b192133..38e7d5b95 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoClientBuilder.cs +++ b/src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; -namespace CommunityToolkit.Aspire.Hosting.Logto.Client; +namespace CommunityToolkit.Aspire.Logto.Client; /// /// Provides methods to configure and add Logto client services to an application builder. diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoConnectionStringHelper.cs b/src/CommunityToolkit.Aspire.Logto.Client/LogtoConnectionStringHelper.cs similarity index 96% rename from src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoConnectionStringHelper.cs rename to src/CommunityToolkit.Aspire.Logto.Client/LogtoConnectionStringHelper.cs index 658cfc46b..1c1e0bb66 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto.Client/LogtoConnectionStringHelper.cs +++ b/src/CommunityToolkit.Aspire.Logto.Client/LogtoConnectionStringHelper.cs @@ -1,6 +1,6 @@ using System.Data.Common; -namespace CommunityToolkit.Aspire.Hosting.Logto.Client; +namespace CommunityToolkit.Aspire.Logto.Client; /// /// Provides utility methods for extracting and validating endpoint information diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests.csproj b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj similarity index 65% rename from tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests.csproj rename to tests/CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj index 5c31fd3d6..f161242a6 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs similarity index 98% rename from tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs rename to tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs index 525e210aa..bbde24f0d 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; -namespace CommunityToolkit.Aspire.Hosting.Logto.Client.Tests; +namespace CommunityToolkit.Aspire.Logto.Client.Tests; public class LogtoClientBuilderIntegrationTests { diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderTests.cs b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderTests.cs similarity index 98% rename from tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderTests.cs rename to tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderTests.cs index 1edafa83f..055da1058 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoClientBuilderTests.cs +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderTests.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -namespace CommunityToolkit.Aspire.Hosting.Logto.Client.Tests; +namespace CommunityToolkit.Aspire.Logto.Client.Tests; public class LogtoClientBuilderTests { diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs similarity index 96% rename from tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs rename to tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs index c7c1bf598..2f3fedd5f 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs @@ -1,4 +1,4 @@ -namespace CommunityToolkit.Aspire.Hosting.Logto.Client.Tests; +namespace CommunityToolkit.Aspire.Logto.Client.Tests; public class LogtoConnectionStringHelperTests { From a2e300c7a2c1a113529c028b79585e6357143e1d Mon Sep 17 00:00:00 2001 From: Axi Date: Tue, 31 Mar 2026 15:02:47 +0300 Subject: [PATCH 16/16] Add `WithDeprecationTracing` method to enable Node.js deprecation tracing in Logto container --- .../LogtoBuilderExtensions.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index d35f0b14f..bc90a11a1 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -47,7 +47,17 @@ public static IResourceBuilder AddLogtoContainer( return builderWithResource; } - + /// + /// Enables Node.js deprecation tracing for the Logto container by setting the + /// NODE_OPTIONS environment variable to '--trace-deprecation'. + /// This allows stack traces to be printed for deprecated API usage. + /// + /// The resource builder for the Logto container resource that will be configured for stack trace logging. + public static void WithDeprecationTracing(this IResourceBuilder builderWithResource) + { + builderWithResource.WithEnvironment("NODE_OPTIONS", "--trace-deprecation"); + } + private static void SetHealthCheck(IDistributedApplicationBuilder builder, IResourceBuilder builderWithResource, string name) {