From cd63a592a5923d312f8168b3b1c282233108bb99 Mon Sep 17 00:00:00 2001 From: Axi Date: Mon, 1 Dec 2025 01:12:24 +0200 Subject: [PATCH 01/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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) { From 63356b80680f5698badf3636659662bdb06ff6a5 Mon Sep 17 00:00:00 2001 From: Axi Date: Thu, 2 Apr 2026 10:35:15 +0300 Subject: [PATCH 17/40] Add `Hosting.Logto.Tests` and `Logto.Client.Tests` to test matrix in GitHub Actions --- .github/workflows/tests.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index ddf4f4c47..b09f5a8d4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -41,6 +41,7 @@ jobs: Hosting.Keycloak.Extensions.Tests, Hosting.KurrentDB.Tests, Hosting.LavinMQ.Tests, + Hosting.Logto.Tests, Hosting.MailPit.Tests, Hosting.McpInspector.Tests, Hosting.Meilisearch.Tests, @@ -71,6 +72,7 @@ jobs: # Client integration tests GoFeatureFlag.Tests, KurrentDB.Tests, + Logto.Client.Tests, MassTransit.RabbitMQ.Tests, Meilisearch.Tests, Microsoft.Data.Sqlite.Tests, From 20f300bbbc42a500ce8ef7fab3e9e446216efd7d Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 15 Apr 2026 13:04:02 +0300 Subject: [PATCH 18/40] Refactor Logto configuration: introduce validation for Logto options and add support for data volume mapping. --- .../LogtoBuilderExtensions.cs | 3 +- .../LogtoClientBuilder.cs | 40 +++++++++++++++++-- .../AppHostTest.cs | 2 +- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index bc90a11a1..57909cb53 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -57,7 +57,7 @@ public static void WithDeprecationTracing(this IResourceBuilder builderWithResource, string name) { @@ -225,6 +225,7 @@ public IResourceBuilder WithDatabase(IResourceBuilder { @@ -155,7 +155,29 @@ public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder return builder; } - private static LogtoOptions GetEndpoint(IConfiguration configuration, string? configurationSectionName, + private static LogtoOptions GetValidatedOptions( + IConfiguration configuration, + string? configurationSectionName, + string? connectionName, + Action? configureOptions = null) + { + var options = GetConfiguredOptions(configuration, configurationSectionName, connectionName); + configureOptions?.Invoke(options); + + if (string.IsNullOrWhiteSpace(options.Endpoint)) + { + throw new InvalidOperationException("Logto Endpoint must be configured."); + } + + if (string.IsNullOrWhiteSpace(options.AppId)) + { + throw new InvalidOperationException("Logto AppId must be configured."); + } + + return options; + } + + private static LogtoOptions GetConfiguredOptions(IConfiguration configuration, string? configurationSectionName, string? connectionName) { var options = new LogtoOptions(); @@ -175,10 +197,20 @@ private static LogtoOptions GetEndpoint(IConfiguration configuration, string? co } } + return options; + } + + private static LogtoOptions GetEndpoint(IConfiguration configuration, string? configurationSectionName, + string? connectionName) + { + var options = GetConfiguredOptions(configuration, configurationSectionName, connectionName); + if (string.IsNullOrWhiteSpace(options.Endpoint)) + { throw new InvalidOperationException( - $"Logto Endpoint must be configured in configuration section '{sectionName}' or via configureOptions."); + $"Logto Endpoint must be configured in configuration section '{configurationSectionName ?? DefaultConfigSectionName}' or via configureOptions."); + } return options; } -} \ No newline at end of file +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs index 86e846537..cced37710 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs @@ -27,4 +27,4 @@ public async Task LogtoResourceStartsAndRespondsOk() Assert.Equal(HttpStatusCode.OK, response.StatusCode); } -} \ No newline at end of file +} From cbd16a8119a02ad64914651eafbd6c64cfb32d22 Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 15 Apr 2026 13:22:20 +0300 Subject: [PATCH 19/40] Refactor `AppHostTest` to use `AspireIntegrationTestFixture`; add `AppHost` project reference to test project --- .../AppHostTest.cs | 21 +++++++------------ ...yToolkit.Aspire.Hosting.Logto.Tests.csproj | 3 ++- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs index cced37710..834be2b0a 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs @@ -1,28 +1,23 @@ using Aspire.Components.Common.Tests; -using Aspire.Hosting; using CommunityToolkit.Aspire.Testing; namespace CommunityToolkit.Aspire.Hosting.Logto.Tests; [RequiresDocker] -public class AppHostTest +public class AppHostTest( + AspireIntegrationTestFixture fixture +) : IClassFixture> { [Fact] public async Task LogtoResourceStartsAndRespondsOk() { - var builder = DistributedApplication.CreateBuilder(); - var postgres = builder.AddPostgres("postgres"); - var logto = builder.AddLogtoContainer("logto", postgres); + const string resourceName = "logto"; - using var app = builder.Build(); - var rns = app.Services.GetRequiredService(); + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(resourceName) + .WaitAsync(TimeSpan.FromMinutes(5)); - 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 httpClient = fixture.CreateHttpClient(resourceName); var response = await httpClient.GetAsync("/"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); 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 index 3c8568429..e928d2eba 100644 --- 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 @@ -1,8 +1,9 @@  + - \ No newline at end of file + From 907c4d52a5a8a36b0d0d39babe2addddf431ff62 Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 15 Apr 2026 13:31:16 +0300 Subject: [PATCH 20/40] Allow null `port` and `adminPort` in `WithResourcePort` to enable random host port assignment. --- .../LogtoBuilderExtensions.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index 57909cb53..67f78fa7b 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -103,16 +103,20 @@ public IResourceBuilder WithDataVolume(string? name = nu /// /// 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 host port to be configured for the primary endpoint. If , Aspire will assign a random host port. + /// The host port to be configured for the administrative endpoint. If , Aspire will assign a random host port. /// The updated resource builder with the configured HTTP endpoints. public IResourceBuilder WithResourcePort( - int port = LogtoContainerResource.DefaultHttpPort, - int adminPort = LogtoContainerResource.DefaultHttpAdminPort) + int? port = null, + int? adminPort = null) { - return builder.WithHttpEndpoint(port, port, + return builder.WithHttpEndpoint( + port: port, + targetPort: LogtoContainerResource.DefaultHttpPort, name: LogtoContainerResource.PrimaryEndpointName) - .WithHttpEndpoint(adminPort, adminPort, + .WithHttpEndpoint( + port: adminPort, + targetPort: LogtoContainerResource.DefaultHttpAdminPort, name: LogtoContainerResource.AdminEndpointName); } @@ -230,4 +234,4 @@ public IResourceBuilder WithDatabase(IResourceBuilder Date: Fri, 17 Apr 2026 12:24:33 +0300 Subject: [PATCH 21/40] Update Logto container tag to v1.38 --- .../LogtoContainerTags.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerTags.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerTags.cs index 29337a40e..e0e82044d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerTags.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerTags.cs @@ -11,6 +11,6 @@ public class LogtoContainerTags /// svhd/logto public const string Image = "svhd/logto"; - /// 1.34 - public const string Tag = "1.34"; + /// 1.38 + public const string Tag = "1.38"; } \ No newline at end of file From 95c6f2c0fa66c64a93703bcb4ae6c27e4fcf8ab4 Mon Sep 17 00:00:00 2001 From: axi Date: Fri, 17 Apr 2026 12:29:04 +0300 Subject: [PATCH 22/40] Refactor `LogtoContainerResource` to `LogtoResource` and update related APIs and tests. Streamline naming for consistency. --- .../AppHost.cs | 2 +- .../LogtoBuilderExtensions.cs | 108 +++++++++--------- ...oContainerResource.cs => LogtoResource.cs} | 2 +- .../{LogtoContainerTags.cs => LogtoTags.cs} | 2 +- .../ResourceCreationTests.cs | 8 +- 5 files changed, 61 insertions(+), 61 deletions(-) rename src/CommunityToolkit.Aspire.Hosting.Logto/{LogtoContainerResource.cs => LogtoResource.cs} (98%) rename src/CommunityToolkit.Aspire.Hosting.Logto/{LogtoContainerTags.cs => LogtoTags.cs} (92%) diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs index d87f5048e..ec17933ff 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs @@ -8,7 +8,7 @@ var cache = builder.AddRedis("redis") .WithDataVolume(); -var logto = builder.AddLogtoContainer("logto", postgres) +var logto = builder.AddLogto("logto", postgres) .WithRedis(cache); diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index 67f78fa7b..25acf2e82 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -5,20 +5,20 @@ namespace CommunityToolkit.Aspire.Hosting.Logto; /// -/// Provides extension methods for configuring and managing Logto container resources +/// Provides extension methods for configuring and managing Logto resources /// within the Aspire hosting application framework. /// public static class LogtoBuilderExtensions { /// - /// Adds a Logto container resource to the Aspire distributed application by configuring it + /// Adds a Logto 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( + /// The distributed application builder to which the Logto resource will be added. + /// The name of the Logto resource. + /// The resource builder for the PostgreSQL server that the Logto will connect to. + /// The resource builder configured for the added Logto resource. + public static IResourceBuilder AddLogto( this IDistributedApplicationBuilder builder, string name, IResourceBuilder postgres) @@ -28,11 +28,11 @@ public static IResourceBuilder AddLogtoContainer( ArgumentNullException.ThrowIfNull(postgres); - var resource = new LogtoContainerResource(name); + var resource = new LogtoResource(name); var builderWithResource = builder .AddResource(resource) - .WithImage(LogtoContainerTags.Image, LogtoContainerTags.Tag) - .WithImageRegistry(LogtoContainerTags.Registry); + .WithImage(LogtoTags.Image, LogtoTags.Tag) + .WithImageRegistry(LogtoTags.Registry); builderWithResource.WithResourcePort(); builderWithResource.WithDatabase(postgres); @@ -48,20 +48,20 @@ public static IResourceBuilder AddLogtoContainer( } /// - /// Enables Node.js deprecation tracing for the Logto container by setting the + /// Enables Node.js deprecation tracing for the Logto 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) + /// The resource builder for the Logto 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) + IResourceBuilder builderWithResource, string name) { - var endpoint = builderWithResource.Resource.GetEndpoint(LogtoContainerResource.PrimaryEndpointName); + var endpoint = builderWithResource.Resource.GetEndpoint(LogtoResource.PrimaryEndpointName); var healthCheckKey = $"{name}_check"; builder.Services.AddHealthChecks() .AddUrlGroup(opt => @@ -72,27 +72,27 @@ private static void SetHealthCheck(IDistributedApplicationBuilder builder, builderWithResource.WithHealthCheck(healthCheckKey); } - /// The resource builder for the Logto container resource to be configured. - extension(IResourceBuilder builder) + /// The resource builder for the Logto resource to be configured. + extension(IResourceBuilder builder) { /// - /// Configures the Logto container resource to use the specified Node.js environment value + /// Configures the Logto 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) + /// The resource builder for the configured Logto 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. + /// Configures the Logto resource with a data volume, allowing persistent storage + /// for the . /// /// 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) + public IResourceBuilder WithDataVolume(string? name = null) { ArgumentNullException.ThrowIfNull(builder); @@ -101,39 +101,39 @@ public IResourceBuilder WithDataVolume(string? name = nu /// - /// Configures HTTP endpoints for the given Logto container resource builder with specified port settings. + /// Configures HTTP endpoints for the given Logto resource builder with specified port settings. /// /// The host port to be configured for the primary endpoint. If , Aspire will assign a random host port. /// The host port to be configured for the administrative endpoint. If , Aspire will assign a random host port. /// The updated resource builder with the configured HTTP endpoints. - public IResourceBuilder WithResourcePort( + public IResourceBuilder WithResourcePort( int? port = null, int? adminPort = null) { return builder.WithHttpEndpoint( port: port, - targetPort: LogtoContainerResource.DefaultHttpPort, - name: LogtoContainerResource.PrimaryEndpointName) + targetPort: LogtoResource.DefaultHttpPort, + name: LogtoResource.PrimaryEndpointName) .WithHttpEndpoint( port: adminPort, - targetPort: LogtoContainerResource.DefaultHttpAdminPort, - name: LogtoContainerResource.AdminEndpointName); + targetPort: LogtoResource.DefaultHttpAdminPort, + name: LogtoResource.AdminEndpointName); } /// - /// Configures the specified Logto container resource to include an administrative endpoint + /// Configures the specified Logto 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) + /// The URL of the administrative endpoint to be used for the Logto resource. + /// The resource builder for the configured Logto resource. + /// https://admin.domain.com + 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. + /// Configures the Logto resource to disable the Admin Console port. /// When set to true and ADMIN_ENDPOINT is unset, it will completely disable the Admin Console. /// /// @@ -141,22 +141,22 @@ public IResourceBuilder WithAdminEndpoint(string url) /// 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) + /// The resource builder for the configured Logto 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 + /// Configures the Logto 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) + /// The resource builder for the configured Logto resource. + public IResourceBuilder WithTrustProxyHeader(bool trustProxyHeader) { return builder.WithEnvironment("TRUST_PROXY_HEADER", trustProxyHeader.ToString()); } @@ -166,29 +166,29 @@ public IResourceBuilder WithTrustProxyHeader(bool trustP /// /// 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) + 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). + /// Configures the Logto 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) + 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. + /// Configures the Logto resource to use a data bind mount with the specified + /// source directory as the data volume for the . /// - /// 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) + /// The host directory to be mounted as the 's data volume. + /// The resource builder for the configured Logto resource. + public IResourceBuilder WithDataBindMount(string source) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(source); @@ -197,12 +197,12 @@ public IResourceBuilder WithDataBindMount(string source) } /// - /// Configures the Logto container resource to use a specified Redis resource for caching or other functionality + /// Configures the Logto 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 for the Redis resource to be used by the Logto resource. /// The resource builder configured with the specified Redis resource. - public IResourceBuilder WithRedis(IResourceBuilder redis) + public IResourceBuilder WithRedis(IResourceBuilder redis) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(redis); @@ -212,12 +212,12 @@ public IResourceBuilder WithRedis(IResourceBuilder - /// Configures the Logto container resource to connect to the specified PostgreSQL database + /// Configures the Logto 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) + /// The resource builder for the configured Logto resource. + public IResourceBuilder WithDatabase(IResourceBuilder postgres) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(postgres); diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs similarity index 98% rename from src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerResource.cs rename to src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs index efec2668b..db1b70ee4 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs @@ -10,7 +10,7 @@ namespace CommunityToolkit.Aspire.Hosting.Logto; /// 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) +public sealed class LogtoResource(string name) : ContainerResource(name), IResourceWithConnectionString { internal const string PrimaryEndpointName = "http"; diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerTags.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs similarity index 92% rename from src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerTags.cs rename to src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs index e0e82044d..4030818c0 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoContainerTags.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs @@ -3,7 +3,7 @@ /// /// Represents a collection of constants for container tags related to the Logto application. /// -public class LogtoContainerTags +public class LogtoTags { /// docker.io public const string Registry = "docker.io"; diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs index 6aef836fa..d96bad446 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs @@ -11,13 +11,13 @@ public void LogtoResourceGetsAdded() var postgres = builder.AddPostgres("postgres"); - builder.AddLogtoContainer("logto", postgres); + builder.AddLogto("logto", postgres); using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); - var resource = Assert.Single(appModel.Resources.OfType()); + var resource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("logto", resource.Name); } @@ -29,13 +29,13 @@ public void LogtoResourceHealthChecks() var postgres = builder.AddPostgres("postgres"); - builder.AddLogtoContainer("logto", postgres); + builder.AddLogto("logto", postgres); using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); - var resource = Assert.Single(appModel.Resources.OfType()); + var resource = Assert.Single(appModel.Resources.OfType()); var result = resource.TryGetAnnotationsOfType(out var annotations); Assert.True(result); From 231ae983ae089db2634b74f9e8c4f1bb8444faff Mon Sep 17 00:00:00 2001 From: axi Date: Fri, 17 Apr 2026 13:16:42 +0300 Subject: [PATCH 23/40] Set default HTTP and admin ports in `WithResourcePort` method of `LogtoBuilderExtensions`. --- .../LogtoBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index 25acf2e82..6167d5c96 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -107,8 +107,8 @@ public IResourceBuilder WithDataVolume(string? name = null) /// The host port to be configured for the administrative endpoint. If , Aspire will assign a random host port. /// The updated resource builder with the configured HTTP endpoints. public IResourceBuilder WithResourcePort( - int? port = null, - int? adminPort = null) + int? port = LogtoResource.DefaultHttpPort, + int? adminPort = LogtoResource.DefaultHttpAdminPort) { return builder.WithHttpEndpoint( port: port, From d67f854c9f67165d189f367dffecafd63d7d24ba Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Tue, 26 May 2026 09:40:45 +1000 Subject: [PATCH 24/40] Update examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj --- .../CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj | 2 +- 1 file changed, 1 insertion(+), 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 832a2091a..d2e600b82 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 From 6311c8bd8b2431b19935f4c3693b34b8f1c7e5e7 Mon Sep 17 00:00:00 2001 From: axi Date: Fri, 5 Jun 2026 14:55:03 +0300 Subject: [PATCH 25/40] Add Logto integration: introduce `LogtoResource` and client builders with OIDC and JWT Bearer configurations. Update APIs, examples, and tests. --- .../AppHost.cs | 7 +- ...oolkit.Aspire.Hosting.Logto.AppHost.csproj | 2 +- ...oolkit.Aspire.Hosting.Logto.ClientJWT.http | 2 +- .../Program.cs | 6 +- ...tyToolkit.Aspire.Hosting.Logto.Client.http | 2 +- .../Program.cs | 13 +- .../appsettings.json | 4 + .../LogtoBuilderExtensions.cs | 397 ++++++++++-------- .../LogtoResource.cs | 40 +- .../LogtoTags.cs | 10 +- .../README.md | 18 +- .../CommunityToolkit.Aspire.Hosting.Logto.cs | 76 ++++ .../LogtoClientBuilder.cs | 24 +- .../LogtoConnectionStringHelper.cs | 14 +- .../CommunityToolkit.Aspire.Logto.Client.cs | 27 ++ .../ResourceCreationTests.cs | 39 +- .../LogtoClientBuilderIntegrationTests.cs | 12 +- .../LogtoClientBuilderTests.cs | 14 +- .../LogtoConnectionStringHelperTests.cs | 10 +- 19 files changed, 471 insertions(+), 246 deletions(-) create mode 100644 src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs create mode 100644 src/CommunityToolkit.Aspire.Logto.Client/api/CommunityToolkit.Aspire.Logto.Client.cs diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs index ec17933ff..269f7b75d 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs @@ -1,5 +1,3 @@ -using CommunityToolkit.Aspire.Hosting.Logto; - var builder = DistributedApplication.CreateBuilder(args); var postgres = builder.AddPostgres("postgres") @@ -9,7 +7,8 @@ .WithDataVolume(); var logto = builder.AddLogto("logto", postgres) - .WithRedis(cache); + .WithRedis(cache) + .WithDatabaseSeeding(); var clientOIDC = builder.AddProject("clientOIDC") @@ -20,4 +19,4 @@ .WaitFor(logto); -builder.Build().Run(); \ No newline at end of file +builder.Build().Run(); 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 d2e600b82..709cff8c1 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/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http index a4e1a7dc0..fa63aff4e 100644 --- a/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http +++ b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/CommunityToolkit.Aspire.Hosting.Logto.ClientJWT.http @@ -1,6 +1,6 @@ @CommunityToolkit.Aspire.Hosting.Logto.ClientJWT_HostAddress = http://localhost:5072 -GET {{CommunityToolkit.Aspire.Hosting.Logto.ClientJWT_HostAddress}}/weatherforecast/ +GET {{CommunityToolkit.Aspire.Hosting.Logto.ClientJWT_HostAddress}}/ Accept: application/json ### diff --git a/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Program.cs b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Program.cs index db14f4123..cfdd35d30 100644 --- a/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Program.cs +++ b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Program.cs @@ -1,6 +1,6 @@ -using CommunityToolkit.Aspire.Logto.Client; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Hosting; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); @@ -9,7 +9,7 @@ .AddLogtoJwtBearer("logto", appIdentification: apiAudience, configureOptions: opt => { - opt.RequireHttpsMetadata = false; + opt.RequireHttpsMetadata = !builder.Environment.IsDevelopment(); }); builder.Services.AddAuthorization(); @@ -31,4 +31,4 @@ }); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.Client.http b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.Client.http index 776076030..69083959b 100644 --- a/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.Client.http +++ b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/CommunityToolkit.Aspire.Hosting.Logto.Client.http @@ -1,6 +1,6 @@ @CommunityToolkit.Aspire.Hosting.Logto.Client_HostAddress = http://localhost:5137 -GET {{CommunityToolkit.Aspire.Hosting.Logto.Client_HostAddress}}/weatherforecast/ +GET {{CommunityToolkit.Aspire.Hosting.Logto.Client_HostAddress}}/ Accept: application/json ### diff --git a/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/Program.cs b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/Program.cs index 2afda04aa..c4b205c59 100644 --- a/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/Program.cs +++ b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/Program.cs @@ -1,6 +1,6 @@ -using CommunityToolkit.Aspire.Logto.Client; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Hosting; using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); @@ -8,12 +8,11 @@ builder.AddLogtoOIDC("logto", logtoOptions: config => { - config.AppId = "s6zda5bqn1qlsjzaiklqn"; - config.AppSecret = "Df77aDt13MG3nSTgo8eKZP2HdeSfbed0"; - -},oidcOptions: opt => + config.AppId = builder.Configuration["Logto:AppId"] ?? string.Empty; + config.AppSecret = builder.Configuration["Logto:AppSecret"] ?? string.Empty; +}, oidcOptions: opt => { - opt.RequireHttpsMetadata = false; + opt.RequireHttpsMetadata = !builder.Environment.IsDevelopment(); }); builder.Services.AddAuthorization(); @@ -73,4 +72,4 @@ context.Response.Redirect("/"); } }); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/appsettings.json b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/appsettings.json index 10f68b8c8..9c9e8aedb 100644 --- a/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/appsettings.json +++ b/examples/logto/CommunityToolkit.Aspire.Logto.ClientOIDC/appsettings.json @@ -5,5 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, + "Logto": { + "AppId": "your-logto-app-id", + "AppSecret": "your-logto-app-secret" + }, "AllowedHosts": "*" } diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index 6167d5c96..610f6acd8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -1,8 +1,9 @@ -using Aspire.Hosting; -using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.ApplicationModel; using Microsoft.Extensions.DependencyInjection; -namespace CommunityToolkit.Aspire.Hosting.Logto; +#pragma warning disable ASPIREATS001 // AspireExport is experimental + +namespace Aspire.Hosting; /// /// Provides extension methods for configuring and managing Logto resources @@ -17,33 +18,35 @@ public static class LogtoBuilderExtensions /// The distributed application builder to which the Logto resource will be added. /// The name of the Logto resource. /// The resource builder for the PostgreSQL server that the Logto will connect to. + /// The PostgreSQL database name Logto should use. + /// The host port to be configured for the primary endpoint. If , Aspire will assign a random host port. + /// The host port to be configured for the administrative endpoint. If , Aspire will assign a random host port. /// The resource builder configured for the added Logto resource. + [AspireExport] public static IResourceBuilder AddLogto( this IDistributedApplicationBuilder builder, string name, - IResourceBuilder postgres) + IResourceBuilder postgres, + string databaseName = "logto_db", + int? port = null, + int? adminPort = null) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(postgres); + ArgumentException.ThrowIfNullOrEmpty(databaseName); var resource = new LogtoResource(name); var builderWithResource = builder .AddResource(resource) - .WithImage(LogtoTags.Image, LogtoTags.Tag) - .WithImageRegistry(LogtoTags.Registry); + .WithImage(LogtoContainerImageTags.Image, LogtoContainerImageTags.Tag) + .WithImageRegistry(LogtoContainerImageTags.Registry); - builderWithResource.WithResourcePort(); - builderWithResource.WithDatabase(postgres); + builderWithResource.WithResourcePort(port, adminPort); + builderWithResource.WithDatabase(postgres, databaseName); SetHealthCheck(builder, builderWithResource, name); - - builderWithResource - .WithEntrypoint("sh") - .WithArgs("-c", "npm run cli db seed -- --swe && npm start"); - - return builderWithResource; } @@ -53,9 +56,13 @@ public static IResourceBuilder AddLogto( /// This allows stack traces to be printed for deprecated API usage. /// /// The resource builder for the Logto resource that will be configured for stack trace logging. - public static void WithDeprecationTracing(this IResourceBuilder builderWithResource) + /// The resource builder for the configured Logto resource. + [AspireExport] + public static IResourceBuilder WithDeprecationTracing(this IResourceBuilder builderWithResource) { - builderWithResource.WithEnvironment("NODE_OPTIONS", "--trace-deprecation"); + ArgumentNullException.ThrowIfNull(builderWithResource); + + return builderWithResource.WithEnvironment("NODE_OPTIONS", "--trace-deprecation"); } private static void SetHealthCheck(IDistributedApplicationBuilder builder, @@ -72,166 +79,204 @@ private static void SetHealthCheck(IDistributedApplicationBuilder builder, builderWithResource.WithHealthCheck(healthCheckKey); } - /// The resource builder for the Logto resource to be configured. - extension(IResourceBuilder builder) + /// + /// Configures the Logto resource to use the specified Node.js environment value + /// by setting the corresponding environment variable. + /// + /// The resource builder for the Logto resource to configure. + /// The value of the Node.js environment variable to set, typically "development", "production", or "test". + /// The resource builder for the configured Logto resource. + [AspireExport] + public static IResourceBuilder WithNodeEnv(this IResourceBuilder builder, string env) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(env); + + return builder.WithEnvironment("NODE_ENV", env); + } + + /// + /// Configures the Logto resource with a data volume, allowing persistent storage. + /// + /// The resource builder for the Logto resource to configure. + /// The optional name of the data volume. If not provided, a default name is generated. + /// The resource builder configured with the specified data volume. + [AspireExport] + public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null) { - /// - /// Configures the Logto 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 resource. - public IResourceBuilder WithNodeEnv(string env) - { - return builder.WithEnvironment("NODE_ENV", env); - } - - /// - /// Configures the Logto resource with a data volume, allowing persistent storage - /// for the . - /// - /// 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 resource builder with specified port settings. - /// - /// The host port to be configured for the primary endpoint. If , Aspire will assign a random host port. - /// The host port to be configured for the administrative endpoint. If , Aspire will assign a random host port. - /// The updated resource builder with the configured HTTP endpoints. - public IResourceBuilder WithResourcePort( - int? port = LogtoResource.DefaultHttpPort, - int? adminPort = LogtoResource.DefaultHttpAdminPort) - { - return builder.WithHttpEndpoint( - port: port, - targetPort: LogtoResource.DefaultHttpPort, - name: LogtoResource.PrimaryEndpointName) - .WithHttpEndpoint( - port: adminPort, - targetPort: LogtoResource.DefaultHttpAdminPort, - name: LogtoResource.AdminEndpointName); - } - - /// - /// Configures the specified Logto resource to include an administrative endpoint - /// with the given URL. - /// - /// The URL of the administrative endpoint to be used for the Logto resource. - /// The resource builder for the configured Logto resource. - /// https://admin.domain.com - public IResourceBuilder WithAdminEndpoint(string url) - { - return builder.WithEnvironment("ADMIN_ENDPOINT", url); - } - - /// - /// Configures the Logto 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 resource. - public IResourceBuilder WithDisableAdminConsole(bool disable) - { - return builder.WithEnvironment("ADMIN_DISABLE_LOCALHOST", disable.ToString()); - } - - /// - /// Configures the Logto 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 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 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 resource to use a data bind mount with the specified - /// source directory as the data volume for the . - /// - /// The host directory to be mounted as the 's data volume. - /// The resource builder for the configured Logto resource. - public IResourceBuilder WithDataBindMount(string source) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(source); - - return builder.WithBindMount(source, "/data"); - } - - /// - /// Configures the Logto 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 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 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 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) - .WithDataVolume("logto-data") - .WaitFor(postgres); - return builder; - } + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/data"); + } + + /// + /// Configures HTTP endpoints for the given Logto resource builder with specified port settings. + /// + /// The resource builder for the Logto resource to configure. + /// The host port to be configured for the primary endpoint. If , Aspire will assign a random host port. + /// The host port to be configured for the administrative endpoint. If , Aspire will assign a random host port. + /// The updated resource builder with the configured HTTP endpoints. + [AspireExport] + public static IResourceBuilder WithResourcePort( + this IResourceBuilder builder, + int? port = null, + int? adminPort = null) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithHttpEndpoint( + port: port, + targetPort: LogtoResource.DefaultHttpPort, + name: LogtoResource.PrimaryEndpointName) + .WithHttpEndpoint( + port: adminPort, + targetPort: LogtoResource.DefaultHttpAdminPort, + name: LogtoResource.AdminEndpointName); + } + + /// + /// Configures the specified Logto resource to include an administrative endpoint with the given URL. + /// + /// The resource builder for the Logto resource to configure. + /// The URL of the administrative endpoint to be used for the Logto resource. + /// The resource builder for the configured Logto resource. + /// https://admin.domain.com + [AspireExport] + public static IResourceBuilder WithAdminEndpoint(this IResourceBuilder builder, string url) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(url); + + return builder.WithEnvironment("ADMIN_ENDPOINT", url); + } + + /// + /// Configures the Logto resource to disable the Admin Console port. + /// + /// The resource builder for the Logto resource to configure. + /// A value indicating whether to disable the Admin Console port. + /// The resource builder for the configured Logto resource. + [AspireExport] + public static IResourceBuilder WithDisableAdminConsole(this IResourceBuilder builder, bool disable) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEnvironment("ADMIN_DISABLE_LOCALHOST", disable.ToString()); + } + + /// + /// Configures the Logto resource to enable or disable the trust proxy header behavior. + /// + /// The resource builder for the Logto resource to configure. + /// A value indicating whether to trust the proxy header. + /// The resource builder for the configured Logto resource. + [AspireExport] + public static IResourceBuilder WithTrustProxyHeader(this IResourceBuilder builder, bool trustProxyHeader) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEnvironment("TRUST_PROXY_HEADER", trustProxyHeader.ToString()); + } + + /// + /// Specifies whether the username is case-sensitive. + /// + /// The resource builder for the Logto resource to configure. + /// A value indicating whether usernames should be treated as case-sensitive. + /// The updated resource builder with the configured case-sensitivity setting. + [AspireExport] + public static IResourceBuilder WithSensitiveUsername(this IResourceBuilder builder, bool sensitiveUsername) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEnvironment("CASE_SENSITIVE_USERNAME", sensitiveUsername.ToString()); + } + + /// + /// Configures the Logto resource to use a secret vault with the specified key encryption key (KEK). + /// + /// The resource builder for the Logto resource to configure. + /// The base64-encoded key encryption key (KEK) for the secret vault. + /// The resource builder for the configured Logto resource. + [AspireExport] + public static IResourceBuilder WithSecretVault(this IResourceBuilder builder, string secretVaultKek) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(secretVaultKek); + + return builder.WithEnvironment("SECRET_VAULT_KEK", secretVaultKek); + } + + /// + /// Configures the Logto resource to use a data bind mount with the specified source directory. + /// + /// The resource builder for the Logto resource to configure. + /// The host directory to be mounted as the data volume. + /// The resource builder for the configured Logto resource. + [AspireExport] + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(source); + + return builder.WithBindMount(source, "/data"); + } + + /// + /// Configures the Logto resource to use a specified Redis resource for caching or other functionality. + /// + /// The resource builder for the Logto resource to configure. + /// The resource builder for the Redis resource to be used by the Logto resource. + /// The resource builder configured with the specified Redis resource. + [AspireExport] + public static IResourceBuilder WithRedis(this IResourceBuilder builder, IResourceBuilder redis) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(redis); + + return builder.WithEnvironment("REDIS_URL", redis.Resource.UriExpression) + .WaitFor(redis); + } + + /// + /// Configures the Logto resource to connect to the specified PostgreSQL database. + /// + /// The resource builder for the Logto resource to configure. + /// The resource builder for the PostgreSQL server to connect to. + /// The PostgreSQL database name Logto should use. + /// The resource builder for the configured Logto resource. + [AspireExport] + public static IResourceBuilder WithDatabase( + this IResourceBuilder builder, + IResourceBuilder postgres, + string databaseName = "logto_db") + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(postgres); + ArgumentException.ThrowIfNullOrEmpty(databaseName); + + var dbUrlBuilder = new ReferenceExpressionBuilder(); + dbUrlBuilder.Append($"{postgres.Resource.UriExpression}/{databaseName}"); + var dbUrl = dbUrlBuilder.Build(); + + return builder.WithEnvironment("DB_URL", dbUrl) + .WaitFor(postgres); + } + + /// + /// Starts Logto by running the database seed command before the application process. + /// + /// The resource builder for the Logto resource to configure. + /// The resource builder for the configured Logto resource. + [AspireExport] + public static IResourceBuilder WithDatabaseSeeding(this IResourceBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder + .WithEntrypoint("sh") + .WithArgs("-c", "npm run cli db seed -- --swe && npm start"); } } + +#pragma warning restore ASPIREATS001 diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs index db1b70ee4..64c6c4e83 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs @@ -1,6 +1,6 @@ -using Aspire.Hosting.ApplicationModel; +#pragma warning disable ASPIREATS001 // AspireExport is experimental -namespace CommunityToolkit.Aspire.Hosting.Logto; +namespace Aspire.Hosting.ApplicationModel; /// /// Represents a containerized resource specific to Logto that extends the base functionality @@ -10,6 +10,7 @@ namespace CommunityToolkit.Aspire.Hosting.Logto; /// This class is designed for use in an application hosting environment and incorporates /// a primary HTTP endpoint with predefined default port configurations. /// +[AspireExport(ExposeProperties = true)] public sealed class LogtoResource(string name) : ContainerResource(name), IResourceWithConnectionString { @@ -18,13 +19,14 @@ public sealed class LogtoResource(string name) internal const int DefaultHttpPort = 3001; internal const int DefaultHttpAdminPort = 3002; + private EndpointReference? _primaryEndpointReference; /// 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); + public EndpointReference PrimaryEndpoint => _primaryEndpointReference ??= 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, @@ -46,21 +48,29 @@ public sealed class LogtoResource(string name) 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 + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create( + $"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + + /// + /// Gets the connection URI expression for the Logto container resource. + /// + public ReferenceExpression UriExpression => + ReferenceExpression.Create( + $"{PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + + IEnumerable> IResourceWithConnectionString.GetConnectionProperties() + { + yield return new("Host", ReferenceExpression.Create($"{Host}")); + yield return new("Port", ReferenceExpression.Create($"{Port}")); + yield return new("Uri", UriExpression); + } +} + +#pragma warning restore ASPIREATS001 diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs index 4030818c0..41d572e9d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs @@ -1,9 +1,9 @@ -namespace CommunityToolkit.Aspire.Hosting.Logto; +namespace Aspire.Hosting; /// /// Represents a collection of constants for container tags related to the Logto application. /// -public class LogtoTags +internal sealed class LogtoContainerImageTags { /// docker.io public const string Registry = "docker.io"; @@ -11,6 +11,6 @@ public class LogtoTags /// svhd/logto public const string Image = "svhd/logto"; - /// 1.38 - public const string Tag = "1.38"; -} \ No newline at end of file + /// 1.39 + public const string Tag = "1.39"; +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/README.md b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md index bebc97886..117ed40be 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md @@ -9,7 +9,7 @@ It includes helpers for wiring Logto to **PostgreSQL** (via `Aspire.Hosting.Post ## Features -- Configure **Logto** to use **PostgreSQL** via `AddLogtoContainer(...)`. +- Configure **Logto** to use **PostgreSQL** via `AddLogto(...)`. - Optional **Redis** integration for caching via `.WithRedis(...)`. - Fluent helpers to set environment variables: - `DB_URL` (Postgres connection string) @@ -29,27 +29,25 @@ It includes helpers for wiring Logto to **PostgreSQL** (via `Aspire.Hosting.Post ```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(); + .AddLogto("logto", postgres) + .WithDatabaseSeeding(); // Advanced setup with Redis and specific configurations var redis = builder.AddRedis("redis"); var logtoSecure = builder - .AddLogtoContainer("logto-secure", postgres, databaseName: "logto_secure_db") + .AddLogto("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: @@ -63,7 +61,7 @@ Logto will be configured with: ## Notes -* Extension methods are in the `CommunityToolkit.Aspire.Hosting.Logto` namespace. -* The resource automatically runs the database seeding command +* Extension methods are in the `Aspire.Hosting` namespace. +* Call `.WithDatabaseSeeding()` to run the database seeding command `npm run cli db seed -- --swe && npm start` on startup. -* Default ports are **3001** (HTTP) and **3002** (Admin). +* Container ports are **3001** (HTTP) and **3002** (Admin). Host ports are random by default unless explicitly configured. diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs new file mode 100644 index 000000000..e803b050e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs @@ -0,0 +1,76 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Aspire.Hosting +{ + public static partial class LogtoBuilderExtensions + { + [AspireExport] + public static ApplicationModel.IResourceBuilder AddLogto(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder postgres, string databaseName = "logto_db", int? port = null, int? adminPort = null) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithAdminEndpoint(this ApplicationModel.IResourceBuilder builder, string url) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithDatabase(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder postgres, string databaseName = "logto_db") { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithDatabaseSeeding(this ApplicationModel.IResourceBuilder builder) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithDeprecationTracing(this ApplicationModel.IResourceBuilder builderWithResource) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithDisableAdminConsole(this ApplicationModel.IResourceBuilder builder, bool disable) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithNodeEnv(this ApplicationModel.IResourceBuilder builder, string env) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithRedis(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder redis) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithResourcePort(this ApplicationModel.IResourceBuilder builder, int? port = null, int? adminPort = null) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithSecretVault(this ApplicationModel.IResourceBuilder builder, string secretVaultKek) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithSensitiveUsername(this ApplicationModel.IResourceBuilder builder, bool sensitiveUsername) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithTrustProxyHeader(this ApplicationModel.IResourceBuilder builder, bool trustProxyHeader) { throw null; } + } +} + +namespace Aspire.Hosting.ApplicationModel +{ + [AspireExport(ExposeProperties = true)] + public sealed partial class LogtoResource : ContainerResource, IResourceWithConnectionString, IResource, IExpressionValue, IValueProvider, IManifestExpressionProvider, IValueWithReferences + { + public LogtoResource(string name) : base(default!, default) { } + + public ReferenceExpression ConnectionStringExpression { get { throw null; } } + + public EndpointReferenceExpression Host { get { throw null; } } + + public EndpointReferenceExpression Port { get { throw null; } } + + public EndpointReference PrimaryEndpoint { get { throw null; } } + + public ReferenceExpression UriExpression { get { throw null; } } + + System.Collections.Generic.IEnumerable> IResourceWithConnectionString.GetConnectionProperties() { throw null; } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs b/src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs index 914d97d20..04f9a8bcb 100644 --- a/src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs +++ b/src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs @@ -2,12 +2,13 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using CommunityToolkit.Aspire.Logto.Client; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; -namespace CommunityToolkit.Aspire.Logto.Client; +namespace Microsoft.Extensions.Hosting; /// /// Provides methods to configure and add Logto client services to an application builder. @@ -59,7 +60,6 @@ public static IServiceCollection AddLogtoOIDC(this IHostApplicationBuilder build opt.Endpoint = options.Endpoint; opt.AppId = options.AppId; opt.AppSecret = options.AppSecret; - logtoOptions?.Invoke(opt); }); builder.Services.Configure(authenticationScheme, opt => { @@ -94,6 +94,10 @@ public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder string? configurationSectionName = DefaultConfigSectionName, Action? configureOptions = null) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(serviceName); + ArgumentException.ThrowIfNullOrEmpty(appIdentification); + return AddLogtoJwtBearer(builder, serviceName, [appIdentification], authenticationScheme, configurationSectionName, configureOptions); } @@ -125,7 +129,7 @@ public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder /// /// Thrown when the builder is null. /// - /// Thrown when serviceName or appIndeficator is missing or invalid. + /// Thrown when serviceName or appIdentification is missing or invalid. /// public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder builder, string serviceName, @@ -134,6 +138,16 @@ public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder string? configurationSectionName = DefaultConfigSectionName, Action? configureOptions = null) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(serviceName); + ArgumentNullException.ThrowIfNull(appIdentification); + + var audiences = appIdentification as string[] ?? appIdentification.ToArray(); + if (audiences.Length == 0 || audiences.Any(string.IsNullOrWhiteSpace)) + { + throw new ArgumentException("At least one non-empty application identifier must be provided.", nameof(appIdentification)); + } + builder.Services .AddOptions(authenticationScheme) .Configure((jwt, configuration) => @@ -147,7 +161,7 @@ public static AuthenticationBuilder AddLogtoJwtBearer(this AuthenticationBuilder ValidateIssuer = true, ValidIssuer = issuer, ValidateAudience = true, - ValidAudiences = appIdentification + ValidAudiences = audiences }; configureOptions?.Invoke(jwt); }); @@ -208,7 +222,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 '{configurationSectionName ?? DefaultConfigSectionName}' or via configureOptions."); + $"Logto Endpoint must be configured in configuration section '{configurationSectionName ?? DefaultConfigSectionName}' or in connection string '{connectionName}'."); } return options; diff --git a/src/CommunityToolkit.Aspire.Logto.Client/LogtoConnectionStringHelper.cs b/src/CommunityToolkit.Aspire.Logto.Client/LogtoConnectionStringHelper.cs index 1c1e0bb66..78655a761 100644 --- a/src/CommunityToolkit.Aspire.Logto.Client/LogtoConnectionStringHelper.cs +++ b/src/CommunityToolkit.Aspire.Logto.Client/LogtoConnectionStringHelper.cs @@ -7,7 +7,7 @@ namespace CommunityToolkit.Aspire.Logto.Client; /// 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 +public static class LogtoConnectionStringHelper { private const string ConnectionStringEndpointKey = "Endpoint"; @@ -33,7 +33,15 @@ public class LogtoConnectionStringHelper return uri.ToString(); } - var builder = new DbConnectionStringBuilder { ConnectionString = connectionString }; + DbConnectionStringBuilder builder; + try + { + builder = new DbConnectionStringBuilder { ConnectionString = connectionString }; + } + catch (ArgumentException) + { + return null; + } if (builder.TryGetValue(ConnectionStringEndpointKey, out var endpointObj) && endpointObj is string endpoint && @@ -44,4 +52,4 @@ endpointObj is string endpoint && return null; } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit.Aspire.Logto.Client/api/CommunityToolkit.Aspire.Logto.Client.cs b/src/CommunityToolkit.Aspire.Logto.Client/api/CommunityToolkit.Aspire.Logto.Client.cs new file mode 100644 index 000000000..33ea10d4f --- /dev/null +++ b/src/CommunityToolkit.Aspire.Logto.Client/api/CommunityToolkit.Aspire.Logto.Client.cs @@ -0,0 +1,27 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace CommunityToolkit.Aspire.Logto.Client +{ + public static partial class LogtoConnectionStringHelper + { + public static string? GetEndpointFromConnectionString(string? connectionString) { throw null; } + } +} + +namespace Microsoft.Extensions.Hosting +{ + public static partial class LogtoClientBuilder + { + public static AspNetCore.Authentication.AuthenticationBuilder AddLogtoJwtBearer(this AspNetCore.Authentication.AuthenticationBuilder builder, string serviceName, System.Collections.Generic.IEnumerable appIdentification, string authenticationScheme = "Bearer", string? configurationSectionName = "Aspire:Logto:Client", System.Action? configureOptions = null) { throw null; } + + public static AspNetCore.Authentication.AuthenticationBuilder AddLogtoJwtBearer(this AspNetCore.Authentication.AuthenticationBuilder builder, string serviceName, string appIdentification, string authenticationScheme = "Bearer", string? configurationSectionName = "Aspire:Logto:Client", System.Action? configureOptions = null) { throw null; } + + public static DependencyInjection.IServiceCollection AddLogtoOIDC(this IHostApplicationBuilder builder, string? connectionName = null, string? configurationSectionName = "Aspire:Logto:Client", string authenticationScheme = "Logto", string cookieScheme = "Logto.Cookie", System.Action? logtoOptions = null, System.Action? oidcOptions = null) { throw 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 d96bad446..4b3e185da 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs @@ -41,4 +41,41 @@ public void LogtoResourceHealthChecks() Assert.True(result); Assert.NotNull(annotations); } -} \ No newline at end of file + + [Fact] + public void LogtoResourceDoesNotOverrideEntrypointByDefault() + { + var builder = DistributedApplication.CreateBuilder(); + + var postgres = builder.AddPostgres("postgres"); + + builder.AddLogto("logto", postgres); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.False(resource.TryGetAnnotationsOfType(out _)); + } + + [Fact] + public void LogtoResourceCanUseSeededStartup() + { + var builder = DistributedApplication.CreateBuilder(); + + var postgres = builder.AddPostgres("postgres"); + + builder.AddLogto("logto", postgres) + .WithDatabaseSeeding(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.True(resource.TryGetAnnotationsOfType(out _)); + } +} diff --git a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs index bbde24f0d..56b38400e 100644 --- a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; namespace CommunityToolkit.Aspire.Logto.Client.Tests; @@ -71,9 +72,9 @@ public async Task AddLogtoOIDC_AllowsOverrideOfAuthenticationScheme() } [Fact] - public void AddLogtoSDKClient_UsesConnectionStringEndpoint_WhenSectionEndpointMissing() + public void AddLogtoOIDC_UsesConnectionStringEndpoint_WhenSectionEndpointMissing() { - // Arrange: удаляем Endpoint из секции, оставляем только в connection string + // Arrange var extraConfig = new Dictionary { ["Aspire:Logto:Client:Endpoint"] = null, @@ -86,12 +87,11 @@ public void AddLogtoSDKClient_UsesConnectionStringEndpoint_WhenSectionEndpointMi builder.AddLogtoOIDC(connectionName: "Logto"); using var host = builder.Build(); - // Assert: как минимум убедимся, что всё собралось - // и LogtoOptions вообще зарегистрированы (если библиотека их регистрирует) + // Assert var optionsMonitor = host.Services.GetService>(); Assert.NotNull(optionsMonitor); - var options = optionsMonitor!.Get("Logto"); // имя схемы + 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); @@ -120,4 +120,4 @@ public void AddLogtoClient_ConfigureSettings_CanOverrideOptions() 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.Logto.Client.Tests/LogtoClientBuilderTests.cs b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderTests.cs index 055da1058..6b3f37568 100644 --- a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderTests.cs +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderTests.cs @@ -6,7 +6,7 @@ namespace CommunityToolkit.Aspire.Logto.Client.Tests; public class LogtoClientBuilderTests { [Fact] - public void AddLogtoSDKClient_ThrowsArgumentNull_WhenBuilderIsNull() + public void AddLogtoOIDC_ThrowsArgumentNull_WhenBuilderIsNull() { IHostApplicationBuilder? builder = null; @@ -17,7 +17,7 @@ public void AddLogtoSDKClient_ThrowsArgumentNull_WhenBuilderIsNull() } [Fact] - public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenEndpointNotConfiguredAnywhere() + public void AddLogtoOIDC_ThrowsInvalidOperation_WhenEndpointNotConfiguredAnywhere() { var builder = Host.CreateApplicationBuilder(); builder.Configuration.AddInMemoryCollection(new Dictionary @@ -32,7 +32,7 @@ public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenEndpointNotConfiguredAn } [Fact] - public void AddLogtoSDKClient_UsesEndpointFromConfiguration_WhenPresent() + public void AddLogtoOIDC_UsesEndpointFromConfiguration_WhenPresent() { var builder = Host.CreateApplicationBuilder(); @@ -50,7 +50,7 @@ public void AddLogtoSDKClient_UsesEndpointFromConfiguration_WhenPresent() } [Fact] - public void AddLogtoSDKClient_UsesEndpointFromConnectionString_WhenConfigDoesNotContainEndpoint() + public void AddLogtoOIDC_UsesEndpointFromConnectionString_WhenConfigDoesNotContainEndpoint() { var builder = Host.CreateApplicationBuilder(); @@ -68,7 +68,7 @@ public void AddLogtoSDKClient_UsesEndpointFromConnectionString_WhenConfigDoesNot } [Fact] - public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenConfigureSettingsClearsEndpoint() + public void AddLogtoOIDC_ThrowsInvalidOperation_WhenConfigureSettingsClearsEndpoint() { var builder = Host.CreateApplicationBuilder(); @@ -89,7 +89,7 @@ public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenConfigureSettingsClears } [Fact] - public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenAppIdIsMissingAfterConfigureSettings() + public void AddLogtoOIDC_ThrowsInvalidOperation_WhenAppIdIsMissingAfterConfigureSettings() { var builder = Host.CreateApplicationBuilder(); @@ -107,4 +107,4 @@ public void AddLogtoSDKClient_ThrowsInvalidOperation_WhenAppIdIsMissingAfterConf Assert.Equal("Logto AppId must be configured.", ex.Message); } -} \ No newline at end of file +} diff --git a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs index 2f3fedd5f..d0c53886f 100644 --- a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs @@ -55,4 +55,12 @@ public void GetEndpointFromConnectionString_ReturnsNull_WhenEndpointKeyMissing() Assert.Null(result); } -} \ No newline at end of file + + [Fact] + public void GetEndpointFromConnectionString_ReturnsNull_WhenConnectionStringIsMalformed() + { + var result = LogtoConnectionStringHelper.GetEndpointFromConnectionString("Endpoint=https://logto.example.com;="); + + Assert.Null(result); + } +} From 98603e55331323fbf00a1797dbd211dde9506a2d Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Mon, 15 Jun 2026 11:49:19 +1000 Subject: [PATCH 26/40] Apply suggestion from @aaronpowell --- .../CommunityToolkit.Aspire.Hosting.Logto.csproj | 1 + 1 file changed, 1 insertion(+) 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 a4e35450f..8c8ce9383 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj @@ -10,6 +10,7 @@ + From dc8e10c1ecf3e94cb727efd305196bb2c0b4adc6 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Mon, 15 Jun 2026 12:20:15 +1000 Subject: [PATCH 27/40] Fixing build errors --- .../CommunityToolkit.Aspire.Hosting.Logto.AppHost.csproj | 2 +- .../CommunityToolkit.Aspire.Logto.Client.Tests.csproj | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 709cff8c1..087daf373 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/tests/CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj index f161242a6..70a687ce9 100644 --- a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj @@ -1,5 +1,7 @@  - + + + From c7eb02e629b0be564f3f5473135e7b0e55bbb92a Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Mon, 15 Jun 2026 18:22:23 +1000 Subject: [PATCH 28/40] Fix Logto test startup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AppHost.cs | 6 +- .../LogtoResource.cs | 5 +- .../CommunityToolkit.Aspire.Hosting.Logto.cs | 76 ------------------- .../AppHostTest.cs | 4 +- 4 files changed, 6 insertions(+), 85 deletions(-) delete mode 100644 src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs index 269f7b75d..88102afd9 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs @@ -1,10 +1,8 @@ var builder = DistributedApplication.CreateBuilder(args); -var postgres = builder.AddPostgres("postgres") - .WithDataVolume(); +var postgres = builder.AddPostgres("postgres"); -var cache = builder.AddRedis("redis") - .WithDataVolume(); +var cache = builder.AddRedis("redis"); var logto = builder.AddLogto("logto", postgres) .WithRedis(cache) diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs index 64c6c4e83..01cae638d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs @@ -56,14 +56,13 @@ public sealed class LogtoResource(string name) /// connection details formatted as a string expression. public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create( - $"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + $"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Url)}"); /// /// Gets the connection URI expression for the Logto container resource. /// public ReferenceExpression UriExpression => - ReferenceExpression.Create( - $"{PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + ReferenceExpression.Create($"{PrimaryEndpoint.Property(EndpointProperty.Url)}"); IEnumerable> IResourceWithConnectionString.GetConnectionProperties() { diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs deleted file mode 100644 index e803b050e..000000000 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs +++ /dev/null @@ -1,76 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -namespace Aspire.Hosting -{ - public static partial class LogtoBuilderExtensions - { - [AspireExport] - public static ApplicationModel.IResourceBuilder AddLogto(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder postgres, string databaseName = "logto_db", int? port = null, int? adminPort = null) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithAdminEndpoint(this ApplicationModel.IResourceBuilder builder, string url) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDatabase(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder postgres, string databaseName = "logto_db") { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDatabaseSeeding(this ApplicationModel.IResourceBuilder builder) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDeprecationTracing(this ApplicationModel.IResourceBuilder builderWithResource) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDisableAdminConsole(this ApplicationModel.IResourceBuilder builder, bool disable) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithNodeEnv(this ApplicationModel.IResourceBuilder builder, string env) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithRedis(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder redis) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithResourcePort(this ApplicationModel.IResourceBuilder builder, int? port = null, int? adminPort = null) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithSecretVault(this ApplicationModel.IResourceBuilder builder, string secretVaultKek) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithSensitiveUsername(this ApplicationModel.IResourceBuilder builder, bool sensitiveUsername) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithTrustProxyHeader(this ApplicationModel.IResourceBuilder builder, bool trustProxyHeader) { throw null; } - } -} - -namespace Aspire.Hosting.ApplicationModel -{ - [AspireExport(ExposeProperties = true)] - public sealed partial class LogtoResource : ContainerResource, IResourceWithConnectionString, IResource, IExpressionValue, IValueProvider, IManifestExpressionProvider, IValueWithReferences - { - public LogtoResource(string name) : base(default!, default) { } - - public ReferenceExpression ConnectionStringExpression { get { throw null; } } - - public EndpointReferenceExpression Host { get { throw null; } } - - public EndpointReferenceExpression Port { get { throw null; } } - - public EndpointReference PrimaryEndpoint { get { throw null; } } - - public ReferenceExpression UriExpression { get { throw null; } } - - System.Collections.Generic.IEnumerable> IResourceWithConnectionString.GetConnectionProperties() { throw null; } - } -} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs index 834be2b0a..41ad4767e 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs @@ -18,8 +18,8 @@ await fixture.ResourceNotificationService .WaitAsync(TimeSpan.FromMinutes(5)); var httpClient = fixture.CreateHttpClient(resourceName); - var response = await httpClient.GetAsync("/"); + var response = await httpClient.GetAsync("/api/status"); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } } From f4476a6dc05a4ee21299165027182b194b579d4f Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 17 Jun 2026 05:47:41 +0000 Subject: [PATCH 29/40] not using persisted db This seems to be messing with local dev auth from postgres. --- .../CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs index 269f7b75d..20dbc3568 100644 --- a/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs +++ b/examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/AppHost.cs @@ -1,7 +1,6 @@ var builder = DistributedApplication.CreateBuilder(args); -var postgres = builder.AddPostgres("postgres") - .WithDataVolume(); +var postgres = builder.AddPostgres("postgres"); var cache = builder.AddRedis("redis") .WithDataVolume(); From 0284f21f14a61071c8bb09302c4b9084009d249d Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 17 Jun 2026 05:52:49 +0000 Subject: [PATCH 30/40] Testing a better endpoint --- .../AppHostTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs index 834be2b0a..41ad4767e 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/AppHostTest.cs @@ -18,8 +18,8 @@ await fixture.ResourceNotificationService .WaitAsync(TimeSpan.FromMinutes(5)); var httpClient = fixture.CreateHttpClient(resourceName); - var response = await httpClient.GetAsync("/"); + var response = await httpClient.GetAsync("/api/status"); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } } From 1c631eb3fc309b6ff6732041e0fa07175ed76b69 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 17 Jun 2026 06:04:57 +0000 Subject: [PATCH 31/40] deleting the generated files --- .../CommunityToolkit.Aspire.Hosting.Logto.cs | 76 ------------------- .../CommunityToolkit.Aspire.Logto.Client.cs | 27 ------- 2 files changed, 103 deletions(-) delete mode 100644 src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs delete mode 100644 src/CommunityToolkit.Aspire.Logto.Client/api/CommunityToolkit.Aspire.Logto.Client.cs diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs deleted file mode 100644 index e803b050e..000000000 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/api/CommunityToolkit.Aspire.Hosting.Logto.cs +++ /dev/null @@ -1,76 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -namespace Aspire.Hosting -{ - public static partial class LogtoBuilderExtensions - { - [AspireExport] - public static ApplicationModel.IResourceBuilder AddLogto(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder postgres, string databaseName = "logto_db", int? port = null, int? adminPort = null) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithAdminEndpoint(this ApplicationModel.IResourceBuilder builder, string url) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDatabase(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder postgres, string databaseName = "logto_db") { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDatabaseSeeding(this ApplicationModel.IResourceBuilder builder) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDeprecationTracing(this ApplicationModel.IResourceBuilder builderWithResource) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithDisableAdminConsole(this ApplicationModel.IResourceBuilder builder, bool disable) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithNodeEnv(this ApplicationModel.IResourceBuilder builder, string env) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithRedis(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder redis) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithResourcePort(this ApplicationModel.IResourceBuilder builder, int? port = null, int? adminPort = null) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithSecretVault(this ApplicationModel.IResourceBuilder builder, string secretVaultKek) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithSensitiveUsername(this ApplicationModel.IResourceBuilder builder, bool sensitiveUsername) { throw null; } - - [AspireExport] - public static ApplicationModel.IResourceBuilder WithTrustProxyHeader(this ApplicationModel.IResourceBuilder builder, bool trustProxyHeader) { throw null; } - } -} - -namespace Aspire.Hosting.ApplicationModel -{ - [AspireExport(ExposeProperties = true)] - public sealed partial class LogtoResource : ContainerResource, IResourceWithConnectionString, IResource, IExpressionValue, IValueProvider, IManifestExpressionProvider, IValueWithReferences - { - public LogtoResource(string name) : base(default!, default) { } - - public ReferenceExpression ConnectionStringExpression { get { throw null; } } - - public EndpointReferenceExpression Host { get { throw null; } } - - public EndpointReferenceExpression Port { get { throw null; } } - - public EndpointReference PrimaryEndpoint { get { throw null; } } - - public ReferenceExpression UriExpression { get { throw null; } } - - System.Collections.Generic.IEnumerable> IResourceWithConnectionString.GetConnectionProperties() { throw null; } - } -} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Logto.Client/api/CommunityToolkit.Aspire.Logto.Client.cs b/src/CommunityToolkit.Aspire.Logto.Client/api/CommunityToolkit.Aspire.Logto.Client.cs deleted file mode 100644 index 33ea10d4f..000000000 --- a/src/CommunityToolkit.Aspire.Logto.Client/api/CommunityToolkit.Aspire.Logto.Client.cs +++ /dev/null @@ -1,27 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -namespace CommunityToolkit.Aspire.Logto.Client -{ - public static partial class LogtoConnectionStringHelper - { - public static string? GetEndpointFromConnectionString(string? connectionString) { throw null; } - } -} - -namespace Microsoft.Extensions.Hosting -{ - public static partial class LogtoClientBuilder - { - public static AspNetCore.Authentication.AuthenticationBuilder AddLogtoJwtBearer(this AspNetCore.Authentication.AuthenticationBuilder builder, string serviceName, System.Collections.Generic.IEnumerable appIdentification, string authenticationScheme = "Bearer", string? configurationSectionName = "Aspire:Logto:Client", System.Action? configureOptions = null) { throw null; } - - public static AspNetCore.Authentication.AuthenticationBuilder AddLogtoJwtBearer(this AspNetCore.Authentication.AuthenticationBuilder builder, string serviceName, string appIdentification, string authenticationScheme = "Bearer", string? configurationSectionName = "Aspire:Logto:Client", System.Action? configureOptions = null) { throw null; } - - public static DependencyInjection.IServiceCollection AddLogtoOIDC(this IHostApplicationBuilder builder, string? connectionName = null, string? configurationSectionName = "Aspire:Logto:Client", string authenticationScheme = "Logto", string cookieScheme = "Logto.Cookie", System.Action? logtoOptions = null, System.Action? oidcOptions = null) { throw null; } - } -} \ No newline at end of file From ec3e661ab3e648c7ded6ea7969cc798c8dda5845 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 17 Jun 2026 06:10:24 +0000 Subject: [PATCH 32/40] Adding missing README.md --- .../README.md | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/CommunityToolkit.Aspire.Logto.Client/README.md diff --git a/src/CommunityToolkit.Aspire.Logto.Client/README.md b/src/CommunityToolkit.Aspire.Logto.Client/README.md new file mode 100644 index 000000000..25ad513a7 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Logto.Client/README.md @@ -0,0 +1,123 @@ +# CommunityToolkit.Aspire.Logto.Client + +Registers [Logto](https://logto.io/) authentication services in the DI container for connecting to a Logto instance using OpenID Connect or JWT Bearer authentication. + +## Getting started + +### Prerequisites + +- A Logto instance (can be hosted via `CommunityToolkit.Aspire.Hosting.Logto`). + +### Install the package + +Install the Aspire Logto Client library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Logto.Client +``` + +## Usage example + +### OpenID Connect (OIDC) Authentication + +In the _Program.cs_ file of your project, call the `AddLogtoOIDC` extension method to register Logto OIDC authentication for use via the dependency injection container. The method takes an optional connection name parameter. + +```csharp +builder.AddLogtoOIDC("logto"); +``` + +### JWT Bearer Authentication + +For API projects that need to validate Logto-issued JWTs, use the `AddLogtoJwtBearer` extension method on the `AuthenticationBuilder`: + +```csharp +builder.Services + .AddAuthentication() + .AddLogtoJwtBearer("logto", "your-app-identifier"); +``` + +## Configuration + +The Aspire Logto Client integration provides multiple options to configure the server connection based on the requirements and conventions of your project. + +### Use a connection string + +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddLogtoOIDC()`: + +```csharp +builder.AddLogtoOIDC("logto"); +``` + +And then the connection string will be retrieved from the `ConnectionStrings` configuration section: + +```json +{ + "ConnectionStrings": { + "logto": "Endpoint=http://localhost:3001" + } +} +``` + +### Use configuration providers + +The Aspire Logto Client integration supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the settings from configuration by using the `Aspire:Logto:Client` key. Example `appsettings.json` that configures some of the options: + +```json +{ + "Aspire": { + "Logto": { + "Client": { + "Endpoint": "http://localhost:3001", + "AppId": "your-app-id", + "AppSecret": "your-app-secret" + } + } + } +} +``` + +### Use inline delegates + +You can also pass the `Action` delegate to set up some or all the options inline, for example to set the AppId from code: + +```csharp +builder.AddLogtoOIDC("logto", logtoOptions: options => +{ + options.AppId = "your-app-id"; + options.AppSecret = "your-app-secret"; +}); +``` + +## AppHost extensions + +In your AppHost project, install the `CommunityToolkit.Aspire.Hosting.Logto` library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Hosting.Logto +``` + +Then, in the _Program.cs_ file of `AppHost`, register a Logto instance and consume the connection using the following methods: + +```csharp +var postgres = builder.AddPostgres("postgres"); + +var logto = builder.AddLogto("logto", postgres); + +var myService = builder.AddProject() + .WithReference(logto); +``` + +The `WithReference` method configures a connection in the `MyService` project named `logto`. In the _Program.cs_ file of `MyService`, the Logto connection can be consumed using: + +```csharp +builder.AddLogtoOIDC("logto"); +``` + +## Additional documentation + +- https://logto.io/docs +- https://github.com/logto-io/csharp + +## Feedback & contributing + +https://github.com/CommunityToolkit/Aspire From 8e2dc530292359c55fa2606d62683beb5434efda Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 16 Jul 2026 01:55:56 +0300 Subject: [PATCH 33/40] Add unit tests for `AddLogtoJwtBearer` to validate configuration and audience handling. --- .../LogtoJwtBearerBuilderTests.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoJwtBearerBuilderTests.cs diff --git a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoJwtBearerBuilderTests.cs b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoJwtBearerBuilderTests.cs new file mode 100644 index 000000000..4dec798ed --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoJwtBearerBuilderTests.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace CommunityToolkit.Aspire.Logto.Client.Tests; + +public class LogtoJwtBearerBuilderTests +{ + [Fact] + public void AddLogtoJwtBearer_ConfiguresIssuerAudiencesAndUserOptions() + { + var builder = WebApplication.CreateBuilder(); + builder.Configuration.AddInMemoryCollection(new Dictionary + { + ["ConnectionStrings:logto"] = "Endpoint=https://logto.example.com/" + }); + + builder.Services.AddAuthentication() + .AddLogtoJwtBearer( + "logto", + ["api-one", "api-two"], + authenticationScheme: "LogtoBearer", + configureOptions: options => options.RequireHttpsMetadata = false); + + using var app = builder.Build(); + var options = app.Services.GetRequiredService>().Get("LogtoBearer"); + + Assert.Equal("https://logto.example.com/oidc", options.Authority); + Assert.Equal("https://logto.example.com/oidc", options.TokenValidationParameters.ValidIssuer); + Assert.Equal(["api-one", "api-two"], options.TokenValidationParameters.ValidAudiences); + Assert.True(options.TokenValidationParameters.ValidateIssuer); + Assert.True(options.TokenValidationParameters.ValidateAudience); + Assert.False(options.RequireHttpsMetadata); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void AddLogtoJwtBearer_RejectsEmptyAudience(string audience) + { + var builder = WebApplication.CreateBuilder(); + + Assert.Throws(() => + builder.Services.AddAuthentication().AddLogtoJwtBearer("logto", audience)); + } +} From 303257cf1a76eaaead5a30b0ee462f240e17f01c Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 16 Jul 2026 01:56:02 +0300 Subject: [PATCH 34/40] Deprecate `WithSensitiveUsername(...)` in favor of tenant-level username case sensitivity settings in Logto 1.41; update Logto container tag to v1.41. --- .../LogtoBuilderExtensions.cs | 1 + src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs | 4 ++-- src/CommunityToolkit.Aspire.Hosting.Logto/README.md | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index 610f6acd8..c52458b6e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -185,6 +185,7 @@ public static IResourceBuilder WithTrustProxyHeader(this IResourc /// A value indicating whether usernames should be treated as case-sensitive. /// The updated resource builder with the configured case-sensitivity setting. [AspireExport] + [Obsolete("CASE_SENSITIVE_USERNAME is deprecated in Logto 1.41. Configure username case sensitivity per tenant in the Logto Console instead.")] public static IResourceBuilder WithSensitiveUsername(this IResourceBuilder builder, bool sensitiveUsername) { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs index 41d572e9d..c9662a886 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs @@ -11,6 +11,6 @@ internal sealed class LogtoContainerImageTags /// svhd/logto public const string Image = "svhd/logto"; - /// 1.39 - public const string Tag = "1.39"; + /// 1.41 + public const string Tag = "1.41"; } diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/README.md b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md index 117ed40be..b0e7fd72a 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md @@ -65,3 +65,6 @@ Logto will be configured with: * Call `.WithDatabaseSeeding()` to run the database seeding command `npm run cli db seed -- --swe && npm start` on startup. * Container ports are **3001** (HTTP) and **3002** (Admin). Host ports are random by default unless explicitly configured. +* In an air-gapped environment, use `.WithDatabaseSeeding(disableAdminPwnedPasswordCheck: true)` to pass Logto's `--dapc` option during initial admin creation. +* `WithSensitiveUsername(...)` is deprecated in Logto 1.41. Configure username case sensitivity in the tenant settings in Logto Console. +* Container ports are **3001** (HTTP) and **3002** (Admin). Host ports are random by default unless explicitly configured; Logto receives the allocated public URLs automatically so browser redirects continue to work. From c2003cadc24caa352830970cf57aa636fefed0b9 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 16 Jul 2026 01:56:07 +0300 Subject: [PATCH 35/40] Add database seeding and upgrade support for Logto with optional pwned password check toggling; update tests and docs. --- .../LogtoBuilderExtensions.cs | 31 ++++++++++++++++--- .../LogtoResource.cs | 4 +++ .../README.md | 5 ++- .../ResourceCreationTests.cs | 27 ++++++++++++++-- 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index c52458b6e..d17da7f97 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -260,23 +260,46 @@ public static IResourceBuilder WithDatabase( dbUrlBuilder.Append($"{postgres.Resource.UriExpression}/{databaseName}"); var dbUrl = dbUrlBuilder.Build(); + builder.Resource.DatabaseUrl = dbUrl; + builder.Resource.PostgresResource = postgres.Resource; + return builder.WithEnvironment("DB_URL", dbUrl) .WaitFor(postgres); } /// - /// Starts Logto by running the database seed command before the application process. + /// Seeds and upgrades the Logto database in a one-shot setup container before Logto starts. /// /// The resource builder for the Logto resource to configure. + /// + /// Disables the Have I Been Pwned check for the initial admin password. Use this only in air-gapped environments. + /// /// The resource builder for the configured Logto resource. [AspireExport] - public static IResourceBuilder WithDatabaseSeeding(this IResourceBuilder builder) + public static IResourceBuilder WithDatabaseSeeding( + this IResourceBuilder builder, + bool disableAdminPwnedPasswordCheck = false) { ArgumentNullException.ThrowIfNull(builder); - return builder + var databaseUrl = builder.Resource.DatabaseUrl + ?? throw new InvalidOperationException("Configure the Logto database before enabling database seeding."); + var postgres = builder.Resource.PostgresResource + ?? throw new InvalidOperationException("Configure the Logto PostgreSQL resource before enabling database seeding."); + var seedArguments = disableAdminPwnedPasswordCheck ? "--swe --dapc" : "--swe"; + + var setup = builder.ApplicationBuilder + .AddContainer($"{builder.Resource.Name}-database-setup", LogtoContainerImageTags.Image, LogtoContainerImageTags.Tag) + .WithImageRegistry(LogtoContainerImageTags.Registry) .WithEntrypoint("sh") - .WithArgs("-c", "npm run cli db seed -- --swe && npm start"); + .WithArgs("-c", $"npm run cli db seed -- {seedArguments} && npm run alteration deploy latest") + .WithEnvironment("DB_URL", databaseUrl) + .WithEnvironment("CI", "true") + .WithParentRelationship(builder.Resource) + .ExcludeFromManifest() + .WaitFor(builder.ApplicationBuilder.CreateResourceBuilder(postgres)); + + return builder.WaitForCompletion(setup); } } diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs index 01cae638d..0977fb728 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs @@ -21,6 +21,10 @@ public sealed class LogtoResource(string name) private EndpointReference? _primaryEndpointReference; + internal ReferenceExpression? DatabaseUrl { get; set; } + + internal PostgresServerResource? PostgresResource { get; set; } + /// 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. diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/README.md b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md index b0e7fd72a..7b9a18678 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md @@ -59,12 +59,11 @@ Logto will be configured with: --- +--- + ## Notes * Extension methods are in the `Aspire.Hosting` namespace. -* Call `.WithDatabaseSeeding()` to run the database seeding command - `npm run cli db seed -- --swe && npm start` on startup. -* Container ports are **3001** (HTTP) and **3002** (Admin). Host ports are random by default unless explicitly configured. * In an air-gapped environment, use `.WithDatabaseSeeding(disableAdminPwnedPasswordCheck: true)` to pass Logto's `--dapc` option during initial admin creation. * `WithSensitiveUsername(...)` is deprecated in Logto 1.41. Configure username case sensitivity in the tenant settings in Logto Console. * Container ports are **3001** (HTTP) and **3002** (Admin). Host ports are random by default unless explicitly configured; Logto receives the allocated public URLs automatically so browser redirects continue to work. diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs index 4b3e185da..a4942815b 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs @@ -61,7 +61,7 @@ public void LogtoResourceDoesNotOverrideEntrypointByDefault() } [Fact] - public void LogtoResourceCanUseSeededStartup() + public async Task LogtoResourceUsesOneShotDatabaseSetup() { var builder = DistributedApplication.CreateBuilder(); @@ -75,7 +75,30 @@ public void LogtoResourceCanUseSeededStartup() var appModel = app.Services.GetRequiredService(); var resource = Assert.Single(appModel.Resources.OfType()); + var setup = Assert.Single(appModel.Resources.OfType(), candidate => + candidate.Name == "logto-database-setup"); - Assert.True(resource.TryGetAnnotationsOfType(out _)); + Assert.False(resource.TryGetAnnotationsOfType(out _)); + Assert.Equal(["-c", "npm run cli db seed -- --swe && npm run alteration deploy latest"], await setup.GetArgumentListAsync()); + Assert.Contains(ManifestPublishingCallbackAnnotation.Ignore, setup.Annotations); + Assert.Contains(resource.Annotations.OfType(), wait => + wait.Resource == setup && wait.WaitType == WaitType.WaitForCompletion); + } + + [Fact] + public async Task LogtoDatabaseSetupCanDisablePwnedPasswordCheck() + { + var builder = DistributedApplication.CreateBuilder(); + var postgres = builder.AddPostgres("postgres"); + + builder.AddLogto("logto", postgres) + .WithDatabaseSeeding(disableAdminPwnedPasswordCheck: true); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var setup = Assert.Single(appModel.Resources.OfType(), candidate => + candidate.Name == "logto-database-setup"); + + Assert.Equal(["-c", "npm run cli db seed -- --swe --dapc && npm run alteration deploy latest"], await setup.GetArgumentListAsync()); } } From a7801244d7a7a99ae7dbb5fa0d6a1b7911c32152 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 16 Jul 2026 01:56:11 +0300 Subject: [PATCH 36/40] Ensure Logto options are fully preserved in OIDC setup; validate endpoint URI scheme and update tests, helper methods, and documentation. --- .../LogtoClientBuilder.cs | 25 +++++++++++++++-- .../LogtoConnectionStringHelper.cs | 21 +++++++++++--- .../README.md | 2 ++ ...tyToolkit.Aspire.Logto.Client.Tests.csproj | 6 +++- .../LogtoClientBuilderIntegrationTests.cs | 28 +++++++++++++++++++ .../LogtoClientBuilderTests.cs | 17 +++++++++++ .../LogtoConnectionStringHelperTests.cs | 10 +++++++ 7 files changed, 101 insertions(+), 8 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs b/src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs index 04f9a8bcb..cce5cad25 100644 --- a/src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs +++ b/src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs @@ -1,8 +1,8 @@ -using Logto.AspNetCore.Authentication; +using CommunityToolkit.Aspire.Logto.Client; +using Logto.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using CommunityToolkit.Aspire.Logto.Client; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -60,6 +60,13 @@ public static IServiceCollection AddLogtoOIDC(this IHostApplicationBuilder build opt.Endpoint = options.Endpoint; opt.AppId = options.AppId; opt.AppSecret = options.AppSecret; + opt.Scopes = options.Scopes; + opt.Resource = options.Resource; + opt.Prompt = options.Prompt; + opt.CallbackPath = options.CallbackPath; + opt.SignedOutCallbackPath = options.SignedOutCallbackPath; + opt.GetClaimsFromUserInfoEndpoint = options.GetClaimsFromUserInfoEndpoint; + opt.CookieDomain = options.CookieDomain; }); builder.Services.Configure(authenticationScheme, opt => { @@ -183,6 +190,8 @@ private static LogtoOptions GetValidatedOptions( throw new InvalidOperationException("Logto Endpoint must be configured."); } + ValidateEndpoint(options.Endpoint); + if (string.IsNullOrWhiteSpace(options.AppId)) { throw new InvalidOperationException("Logto AppId must be configured."); @@ -225,6 +234,16 @@ private static LogtoOptions GetEndpoint(IConfiguration configuration, string? co $"Logto Endpoint must be configured in configuration section '{configurationSectionName ?? DefaultConfigSectionName}' or in connection string '{connectionName}'."); } + ValidateEndpoint(options.Endpoint); + return options; } -} + + private static void ValidateEndpoint(string endpoint) + { + if (!LogtoConnectionStringHelper.TryCreateHttpUri(endpoint, out _)) + { + throw new InvalidOperationException("Logto Endpoint must be an absolute HTTP or HTTPS URI."); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Logto.Client/LogtoConnectionStringHelper.cs b/src/CommunityToolkit.Aspire.Logto.Client/LogtoConnectionStringHelper.cs index 78655a761..4698a8258 100644 --- a/src/CommunityToolkit.Aspire.Logto.Client/LogtoConnectionStringHelper.cs +++ b/src/CommunityToolkit.Aspire.Logto.Client/LogtoConnectionStringHelper.cs @@ -27,8 +27,8 @@ public static class LogtoConnectionStringHelper { return null; } - - if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + + if (TryCreateHttpUri(connectionString, out var uri)) { return uri.ToString(); } @@ -45,11 +45,24 @@ public static class LogtoConnectionStringHelper if (builder.TryGetValue(ConnectionStringEndpointKey, out var endpointObj) && endpointObj is string endpoint && - Uri.TryCreate(endpoint, UriKind.Absolute, out var endpointUri)) + TryCreateHttpUri(endpoint, out var endpointUri)) { return endpointUri.ToString(); } return null; } -} + + internal static bool TryCreateHttpUri(string endpoint, out Uri uri) + { + if (Uri.TryCreate(endpoint, UriKind.Absolute, out var candidate) && + (candidate.Scheme == Uri.UriSchemeHttp || candidate.Scheme == Uri.UriSchemeHttps)) + { + uri = candidate; + return true; + } + + uri = null!; + return false; + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Logto.Client/README.md b/src/CommunityToolkit.Aspire.Logto.Client/README.md index 25ad513a7..0f620cd9f 100644 --- a/src/CommunityToolkit.Aspire.Logto.Client/README.md +++ b/src/CommunityToolkit.Aspire.Logto.Client/README.md @@ -76,6 +76,8 @@ The Aspire Logto Client integration supports [Microsoft.Extensions.Configuration } ``` +All properties exposed by `LogtoOptions`, including scopes, resource, prompt, callback paths, user-info claims, and cookie domain, can be configured in this section or through the inline delegate. The endpoint must be an absolute HTTP or HTTPS URI. + ### Use inline delegates You can also pass the `Action` delegate to set up some or all the options inline, for example to set the AppId from code: diff --git a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj index 70a687ce9..9599d082b 100644 --- a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/CommunityToolkit.Aspire.Logto.Client.Tests.csproj @@ -1,4 +1,8 @@  + + Exe + + @@ -7,4 +11,4 @@ - \ No newline at end of file + diff --git a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs index 56b38400e..bba5b97d9 100644 --- a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderIntegrationTests.cs @@ -120,4 +120,32 @@ public void AddLogtoClient_ConfigureSettings_CanOverrideOptions() Assert.StartsWith("https://overridden.example.com", options.Endpoint); Assert.Equal("overridden-app-id", options.AppId); } + + [Fact] + public void AddLogtoOIDC_PreservesAllLogtoOptions() + { + var builder = CreateBuilderWithBaseConfig(); + + builder.AddLogtoOIDC(logtoOptions: options => + { + options.Scopes = ["openid", "email"]; + options.Resource = "https://api.example.com"; + options.Prompt = "consent"; + options.CallbackPath = "/custom-callback"; + options.SignedOutCallbackPath = "/custom-signout"; + options.GetClaimsFromUserInfoEndpoint = true; + options.CookieDomain = "example.com"; + }); + + using var host = builder.Build(); + var options = host.Services.GetRequiredService>().Get("Logto"); + + Assert.Equal(["openid", "email"], options.Scopes); + Assert.Equal("https://api.example.com", options.Resource); + Assert.Equal("consent", options.Prompt); + Assert.Equal("/custom-callback", options.CallbackPath); + Assert.Equal("/custom-signout", options.SignedOutCallbackPath); + Assert.True(options.GetClaimsFromUserInfoEndpoint); + Assert.Equal("example.com", options.CookieDomain); + } } diff --git a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderTests.cs b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderTests.cs index 6b3f37568..0ac72c4ed 100644 --- a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderTests.cs +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoClientBuilderTests.cs @@ -107,4 +107,21 @@ public void AddLogtoOIDC_ThrowsInvalidOperation_WhenAppIdIsMissingAfterConfigure Assert.Equal("Logto AppId must be configured.", ex.Message); } + + [Theory] + [InlineData("relative/path")] + [InlineData("ftp://logto.example.com")] + public void AddLogtoOIDC_ThrowsInvalidOperation_WhenEndpointIsNotHttp(string endpoint) + { + var builder = Host.CreateApplicationBuilder(); + builder.Configuration.AddInMemoryCollection(new Dictionary + { + ["Aspire:Logto:Client:Endpoint"] = endpoint, + ["Aspire:Logto:Client:AppId"] = "test-app-id" + }); + + var ex = Assert.Throws(() => builder.AddLogtoOIDC()); + + Assert.Equal("Logto Endpoint must be an absolute HTTP or HTTPS URI.", ex.Message); + } } diff --git a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs index d0c53886f..c4a801397 100644 --- a/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs +++ b/tests/CommunityToolkit.Aspire.Logto.Client.Tests/LogtoConnectionStringHelperTests.cs @@ -45,6 +45,16 @@ public void GetEndpointFromConnectionString_ReturnsNull_WhenEndpointIsNotValidUr Assert.Null(result); } + [Theory] + [InlineData("ftp://logto.example.com")] + [InlineData("file:///tmp/logto")] + public void GetEndpointFromConnectionString_ReturnsNull_WhenEndpointIsNotHttp(string connectionString) + { + var result = LogtoConnectionStringHelper.GetEndpointFromConnectionString(connectionString); + + Assert.Null(result); + } + [Fact] public void GetEndpointFromConnectionString_ReturnsNull_WhenEndpointKeyMissing() { From 432743a71172ba2ea0f1088f5615515a98025559 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 16 Jul 2026 01:56:15 +0300 Subject: [PATCH 37/40] Add support for CORS-safe local Admin Console URLs in Logto; update examples, tests, and documentation. --- .../Program.cs | 2 +- .../LogtoBuilderExtensions.cs | 33 +++++++++++++++ .../README.md | 42 ++++++++++++++++--- .../ResourceCreationTests.cs | 38 +++++++++++++++++ 4 files changed, 109 insertions(+), 6 deletions(-) diff --git a/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Program.cs b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Program.cs index cfdd35d30..01cd09a6e 100644 --- a/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Program.cs +++ b/examples/logto/CommunityToolkit.Aspire.Logto.ClientJWT/Program.cs @@ -4,7 +4,7 @@ var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -const string apiAudience = "http://localhost:5072/"; +const string apiAudience = "http://127.0.0.1:9234/"; builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddLogtoJwtBearer("logto", appIdentification: apiAudience, configureOptions: opt => diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index d17da7f97..64583b7f5 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -44,12 +44,45 @@ public static IResourceBuilder AddLogto( .WithImageRegistry(LogtoContainerImageTags.Registry); builderWithResource.WithResourcePort(port, adminPort); + builderWithResource.WithEnvironment(context => + { + // Logto uses these public URLs for browser redirects and Admin Console API calls. + // They are only concrete after Aspire allocates the run-mode host ports. + if (resource.PrimaryEndpoint.IsAllocated) + { + context.EnvironmentVariables.TryAdd("ENDPOINT", resource.PrimaryEndpoint.Url); + } + + var adminEndpoint = resource.GetEndpoint(LogtoResource.AdminEndpointName); + if (adminEndpoint.IsAllocated) + { + context.EnvironmentVariables.TryAdd("ADMIN_ENDPOINT", GetCorsSafeLocalAdminUrl(adminEndpoint.Url)); + } + }); + if (builder.ExecutionContext.IsRunMode) + { + builderWithResource.WithUrlForEndpoint(LogtoResource.AdminEndpointName, url => + url.Url = GetCorsSafeLocalAdminUrl(url.Url)); + } builderWithResource.WithDatabase(postgres, databaseName); SetHealthCheck(builder, builderWithResource, name); return builderWithResource; } + private static string GetCorsSafeLocalAdminUrl(string url) + { + var uri = new Uri(url); + + if (!uri.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + return url; + } + + var builder = new UriBuilder(uri) { Host = "127.0.0.1" }; + return builder.Uri.GetLeftPart(UriPartial.Authority); + } + /// /// Enables Node.js deprecation tracing for the Logto by setting the /// NODE_OPTIONS environment variable to '--trace-deprecation'. diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/README.md b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md index 7b9a18678..f74105329 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md @@ -12,6 +12,7 @@ It includes helpers for wiring Logto to **PostgreSQL** (via `Aspire.Hosting.Post - Configure **Logto** to use **PostgreSQL** via `AddLogto(...)`. - Optional **Redis** integration for caching via `.WithRedis(...)`. - Fluent helpers to set environment variables: + - `ENDPOINT` (automatically set from the allocated primary endpoint during local runs) - `DB_URL` (Postgres connection string) - `REDIS_URL` - `NODE_ENV` @@ -44,26 +45,57 @@ var logtoSecure = builder .WithRedis(redis) .WithAdminEndpoint("https://admin.example.com") .WithDisableAdminConsole(false) - .WithTrustProxyHeader(true) // optional override, default is already true - .WithSensitiveUsername(true) - .WithNodeEnv("production"); + .WithTrustProxyHeader(true); // needed when TLS is terminated by a reverse proxy ``` 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(...)`) +* `ENDPOINT=...` (automatically set to the allocated public HTTP URL during local runs) +* `ADMIN_ENDPOINT=...` (automatically set to the allocated Admin Console URL during local runs, or overridden with `.WithAdminEndpoint(...)`) +* `NODE_ENV` (when explicitly configured with `.WithNodeEnv(...)`) * Auto-configured health checks on `/api/status`. --- +## Local Admin Console and CORS + +Logto Admin Console and the Logto API use separate ports. During a local Aspire run, both ports are exposed through dynamically allocated loopback proxy URLs. For example: + +```text +Admin Console: http://127.0.0.1:10612 +Logto API: http://localhost:10611 +``` + +Logto's production CORS policy intentionally rejects localhost origins when both its internal Admin URL and an external Admin endpoint are present. The symptom is that pages in Admin Console fail to load data or create resources such as Applications, and the browser reports an error similar to: + +```text +Access to fetch at 'http://localhost:10611/api/resources' from origin +'http://localhost:10612' has been blocked by CORS policy: +No 'Access-Control-Allow-Origin' header is present. +``` + +This integration handles the local Aspire scenario automatically: + +- `ENDPOINT` is populated from the allocated Aspire API proxy URL. +- The local `ADMIN_ENDPOINT` and the Admin link shown in Aspire Dashboard use `127.0.0.1` with the allocated Admin proxy port. +- Logto keeps its normal production startup, so the compiled Admin Console is served instead of attempting to contact the unavailable Vite development server. + +> [!IMPORTANT] +> Open the exact `admin` URL shown in Aspire Dashboard. Do not replace `127.0.0.1` with `localhost`: Logto 1.41 intentionally removes `localhost` from its production CORS allowlist when an external Admin endpoint is configured. + +Do not use the direct Docker container port, because it may not match `ADMIN_ENDPOINT`. After changing endpoints, restart the Logto resource and reload the browser without cache. + +For HTTPS termination at a reverse proxy, additionally configure `.WithTrustProxyHeader(true)` and ensure the proxy sends `X-Forwarded-Proto`. + --- ## Notes * Extension methods are in the `Aspire.Hosting` namespace. +* Call `.WithDatabaseSeeding()` to run database seeding and the required Logto schema alterations in a one-shot setup container before Logto starts. This is required when upgrading an existing database to Logto 1.40 or later. +* Local Admin links use `127.0.0.1` rather than `localhost` to remain compatible with Logto 1.41 production CORS checks while still serving the compiled Admin Console. * In an air-gapped environment, use `.WithDatabaseSeeding(disableAdminPwnedPasswordCheck: true)` to pass Logto's `--dapc` option during initial admin creation. * `WithSensitiveUsername(...)` is deprecated in Logto 1.41. Configure username case sensitivity in the tenant settings in Logto Console. * Container ports are **3001** (HTTP) and **3002** (Admin). Host ports are random by default unless explicitly configured; Logto receives the allocated public URLs automatically so browser redirects continue to work. diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs index a4942815b..1a95c9a55 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs @@ -1,9 +1,47 @@ using Aspire.Hosting; +using CommunityToolkit.Aspire.Testing; + namespace CommunityToolkit.Aspire.Hosting.Logto.Tests; public class ResourceCreationTests { + [Fact] + public async Task LogtoResourceUsesAllocatedPublicEndpoints() + { + var builder = DistributedApplication.CreateBuilder(); + var postgres = builder.AddPostgres("postgres"); + + var logto = builder.AddLogto("logto", postgres) + .WithEndpoint("http", endpoint => + endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 49101)) + .WithEndpoint("admin", endpoint => + endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 49102)); + + var environment = await logto.Resource.GetEnvironmentVariablesAsync(); + + Assert.Equal("http://localhost:49101", environment["ENDPOINT"]); + Assert.Equal("http://127.0.0.1:49102", environment["ADMIN_ENDPOINT"]); + } + + [Fact] + public async Task ExplicitAdminEndpointOverridesAllocatedEndpoint() + { + var builder = DistributedApplication.CreateBuilder(); + var postgres = builder.AddPostgres("postgres"); + + var logto = builder.AddLogto("logto", postgres) + .WithEndpoint("http", endpoint => + endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 49101)) + .WithEndpoint("admin", endpoint => + endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 49102)) + .WithAdminEndpoint("https://admin.example.com"); + + var environment = await logto.Resource.GetEnvironmentVariablesAsync(); + + Assert.Equal("https://admin.example.com", environment["ADMIN_ENDPOINT"]); + } + [Fact] public void LogtoResourceGetsAdded() { From b86cd02b12d4f191912b4d00925ba8aad4c8e333 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 16 Jul 2026 02:22:40 +0300 Subject: [PATCH 38/40] Align Logto hosting integration with toolkit conventions --- ...mmunityToolkit.Aspire.Hosting.Logto.csproj | 1 + .../LogtoBuilderExtensions.cs | 29 +++- .../LogtoResource.cs | 4 +- .../README.md | 125 ++++++++---------- .../ResourceCreationTests.cs | 53 +++++++- 5 files changed, 131 insertions(+), 81 deletions(-) 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 8c8ce9383..5a986ee48 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/CommunityToolkit.Aspire.Hosting.Logto.csproj @@ -4,6 +4,7 @@ .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 index 64583b7f5..d10e4dc3d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -1,5 +1,6 @@ using Aspire.Hosting.ApplicationModel; using Microsoft.Extensions.DependencyInjection; +using System.ComponentModel; #pragma warning disable ASPIREATS001 // AspireExport is experimental @@ -25,7 +26,7 @@ public static class LogtoBuilderExtensions [AspireExport] public static IResourceBuilder AddLogto( this IDistributedApplicationBuilder builder, - string name, + [ResourceName] string name, IResourceBuilder postgres, string databaseName = "logto_db", int? port = null, @@ -164,7 +165,8 @@ public static IResourceBuilder WithResourcePort( .WithHttpEndpoint( port: adminPort, targetPort: LogtoResource.DefaultHttpAdminPort, - name: LogtoResource.AdminEndpointName); + name: LogtoResource.AdminEndpointName) + .WithEndpoint(LogtoResource.AdminEndpointName, endpoint => endpoint.ExcludeReferenceEndpoint = true); } /// @@ -180,7 +182,9 @@ public static IResourceBuilder WithAdminEndpoint(this IResourceBu ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(url); - return builder.WithEnvironment("ADMIN_ENDPOINT", url); + return builder + .WithEnvironment("ADMIN_ENDPOINT", url) + .WithUrlForEndpoint(LogtoResource.AdminEndpointName, annotation => annotation.Url = url); } /// @@ -217,7 +221,8 @@ public static IResourceBuilder WithTrustProxyHeader(this IResourc /// The resource builder for the Logto resource to configure. /// A value indicating whether usernames should be treated as case-sensitive. /// The updated resource builder with the configured case-sensitivity setting. - [AspireExport] + [AspireExportIgnore(Reason = "CASE_SENSITIVE_USERNAME was removed in Logto 1.41 and is retained only for source compatibility.")] + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("CASE_SENSITIVE_USERNAME is deprecated in Logto 1.41. Configure username case sensitivity per tenant in the Logto Console instead.")] public static IResourceBuilder WithSensitiveUsername(this IResourceBuilder builder, bool sensitiveUsername) { @@ -315,14 +320,26 @@ public static IResourceBuilder WithDatabaseSeeding( { ArgumentNullException.ThrowIfNull(builder); + if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + return builder; + } + var databaseUrl = builder.Resource.DatabaseUrl ?? throw new InvalidOperationException("Configure the Logto database before enabling database seeding."); var postgres = builder.Resource.PostgresResource ?? throw new InvalidOperationException("Configure the Logto PostgreSQL resource before enabling database seeding."); + var setupName = $"{builder.Resource.Name}-database-setup"; + if (builder.ApplicationBuilder.TryCreateResourceBuilder(setupName, out _)) + { + return builder; + } + var seedArguments = disableAdminPwnedPasswordCheck ? "--swe --dapc" : "--swe"; var setup = builder.ApplicationBuilder - .AddContainer($"{builder.Resource.Name}-database-setup", LogtoContainerImageTags.Image, LogtoContainerImageTags.Tag) + .AddResource(new LogtoDatabaseSetupResource(setupName)) + .WithImage(LogtoContainerImageTags.Image, LogtoContainerImageTags.Tag) .WithImageRegistry(LogtoContainerImageTags.Registry) .WithEntrypoint("sh") .WithArgs("-c", $"npm run cli db seed -- {seedArguments} && npm run alteration deploy latest") @@ -336,4 +353,4 @@ public static IResourceBuilder WithDatabaseSeeding( } } -#pragma warning restore ASPIREATS001 +#pragma warning restore ASPIREATS001 \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs index 0977fb728..deced7cc2 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs @@ -76,4 +76,6 @@ IEnumerable> IResourceWithConnectionSt } } -#pragma warning restore ASPIREATS001 +internal sealed class LogtoDatabaseSetupResource(string name) : ContainerResource(name); + +#pragma warning restore ASPIREATS001 \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/README.md b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md index f74105329..879ba1de9 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/README.md @@ -1,101 +1,82 @@ -# Logto Hosting Extensions for .NET Aspire +# Logto hosting integration -## Overview +Use this integration to model, configure, and orchestrate a [Logto](https://logto.io/) identity service in an Aspire distributed application. It connects Logto to PostgreSQL, optionally connects Redis, exposes the public and Admin Console endpoints, and provides a one-shot database setup command for local runs. -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. - ---- +## Getting started -## Features +Install the hosting integration in the AppHost directory: -- Configure **Logto** to use **PostgreSQL** via `AddLogto(...)`. -- Optional **Redis** integration for caching via `.WithRedis(...)`. -- Fluent helpers to set environment variables: - - `ENDPOINT` (automatically set from the allocated primary endpoint during local runs) - - `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`. - ---- +```console +aspire add CommunityToolkit.Aspire.Hosting.Logto +``` -## Usage (AppHost) +## Usage example ```csharp -using Aspire.Hosting; -using Aspire.Hosting.Postgres; +var builder = DistributedApplication.CreateBuilder(args); + var postgres = builder.AddPostgres("postgres"); +var redis = builder.AddRedis("redis"); -// Basic setup connecting to Postgres -var logto = builder - .AddLogto("logto", postgres) +var logto = builder.AddLogto("logto", postgres) + .WithRedis(redis) .WithDatabaseSeeding(); -// Advanced setup with Redis and specific configurations -var redis = builder.AddRedis("redis"); +builder.AddProject("api") + .WithReference(logto) + .WaitFor(logto); -var logtoSecure = builder - .AddLogto("logto-secure", postgres, databaseName: "logto_secure_db") - .WithRedis(redis) - .WithAdminEndpoint("https://admin.example.com") - .WithDisableAdminConsole(false) - .WithTrustProxyHeader(true); // needed when TLS is terminated by a reverse proxy +builder.Build().Run(); ``` -Logto will be configured with: +The same integration is exported to a TypeScript AppHost: -* `DB_URL=postgresql://.../logto_db` (constructed from the Postgres resource) -* `REDIS_URL=...` (when Redis is attached with `.WithRedis(...)`) -* `ENDPOINT=...` (automatically set to the allocated public HTTP URL during local runs) -* `ADMIN_ENDPOINT=...` (automatically set to the allocated Admin Console URL during local runs, or overridden with `.WithAdminEndpoint(...)`) -* `NODE_ENV` (when explicitly configured with `.WithNodeEnv(...)`) -* Auto-configured health checks on `/api/status`. +```typescript +const postgres = await builder.addPostgres("postgres"); +const redis = await builder.addRedis("redis"); +const logto = await builder.addLogto("logto", postgres); +const api = await builder.addProject("api", "../MyApi/MyApi.csproj"); ---- +await logto.withRedis(redis); +await logto.withDatabaseSeeding(); +await api.withReference(logto); +await api.waitFor(logto); +``` -## Local Admin Console and CORS +`AddLogto` configures `DB_URL`, `ENDPOINT`, `ADMIN_ENDPOINT`, the `/api/status` health check, and the dependency on PostgreSQL. `WithRedis` adds `REDIS_URL` and the Redis dependency. -Logto Admin Console and the Logto API use separate ports. During a local Aspire run, both ports are exposed through dynamically allocated loopback proxy URLs. For example: +### Database setup -```text -Admin Console: http://127.0.0.1:10612 -Logto API: http://localhost:10611 -``` +Call `WithDatabaseSeeding()` to seed the database and deploy required schema alterations in a one-shot setup container before Logto starts. The helper is run-only and is excluded from published manifests. Repeated calls do not create duplicate setup resources. -Logto's production CORS policy intentionally rejects localhost origins when both its internal Admin URL and an external Admin endpoint are present. The symptom is that pages in Admin Console fail to load data or create resources such as Applications, and the browser reports an error similar to: +For an air-gapped environment, use `WithDatabaseSeeding(disableAdminPwnedPasswordCheck: true)` to pass Logto's `--dapc` option during initial administrator creation. -```text -Access to fetch at 'http://localhost:10611/api/resources' from origin -'http://localhost:10612' has been blocked by CORS policy: -No 'Access-Control-Allow-Origin' header is present. -``` +### Local Admin Console and CORS + +Logto's API and Admin Console use separate endpoints. During local Aspire runs, the integration sets `ENDPOINT` to the allocated API URL and normalizes the Admin Console URL to `127.0.0.1`. This is required by Logto 1.41's production CORS behavior. + +Open the exact `admin` URL shown in the Aspire Dashboard. Replacing `127.0.0.1` with `localhost` can cause requests such as `/api/resources` to fail CORS preflight checks and prevent Applications from being created. `WithAdminEndpoint("https://admin.example.com")` updates both Logto's `ADMIN_ENDPOINT` and the Dashboard URL. + +When TLS terminates at a reverse proxy, configure `WithTrustProxyHeader(true)` and ensure that the proxy sends `X-Forwarded-Proto`. -This integration handles the local Aspire scenario automatically: +## Connection properties -- `ENDPOINT` is populated from the allocated Aspire API proxy URL. -- The local `ADMIN_ENDPOINT` and the Admin link shown in Aspire Dashboard use `127.0.0.1` with the allocated Admin proxy port. -- Logto keeps its normal production startup, so the compiled Admin Console is served instead of attempting to contact the unavailable Vite development server. +When another resource uses `WithReference(logto)`, Aspire injects the connection string in the standard `ConnectionStrings__{resourceName}` environment variable. The connection string has the form `Endpoint={Uri}`. -> [!IMPORTANT] -> Open the exact `admin` URL shown in Aspire Dashboard. Do not replace `127.0.0.1` with `localhost`: Logto 1.41 intentionally removes `localhost` from its production CORS allowlist when an external Admin endpoint is configured. +| Property | Description | Example | +| --- | --- | --- | +| `Host` | Host name of the public Logto endpoint. | `localhost` | +| `Port` | Allocated port of the public Logto endpoint. | `10611` | +| `Uri` | Complete public Logto endpoint URL. | `http://localhost:10611` | -Do not use the direct Docker container port, because it may not match `ADMIN_ENDPOINT`. After changing endpoints, restart the Logto resource and reload the browser without cache. +The Admin Console endpoint is intentionally excluded from service-reference endpoint expansion; consumers receive the public Logto endpoint only. -For HTTPS termination at a reverse proxy, additionally configure `.WithTrustProxyHeader(true)` and ensure the proxy sends `X-Forwarded-Proto`. +## Additional documentation ---- +- [Logto documentation](https://docs.logto.io/) +- [Logto configuration](https://docs.logto.io/logto-oss/using-cli/logto-config) +- [Aspire service discovery](https://learn.microsoft.com/dotnet/aspire/service-discovery/overview) -## Notes +## Feedback & contributing -* Extension methods are in the `Aspire.Hosting` namespace. -* Call `.WithDatabaseSeeding()` to run database seeding and the required Logto schema alterations in a one-shot setup container before Logto starts. This is required when upgrading an existing database to Logto 1.40 or later. -* Local Admin links use `127.0.0.1` rather than `localhost` to remain compatible with Logto 1.41 production CORS checks while still serving the compiled Admin Console. -* In an air-gapped environment, use `.WithDatabaseSeeding(disableAdminPwnedPasswordCheck: true)` to pass Logto's `--dapc` option during initial admin creation. -* `WithSensitiveUsername(...)` is deprecated in Logto 1.41. Configure username case sensitivity in the tenant settings in Logto Console. -* Container ports are **3001** (HTTP) and **3002** (Admin). Host ports are random by default unless explicitly configured; Logto receives the allocated public URLs automatically so browser redirects continue to work. +Report bugs and request features in the [CommunityToolkit/Aspire issue tracker](https://github.com/CommunityToolkit/Aspire/issues). Contributions are welcome; see the repository's [contributing guide](https://github.com/CommunityToolkit/Aspire/blob/main/CONTRIBUTING.md). diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs index 1a95c9a55..5bb60f44e 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs @@ -1,5 +1,5 @@ using Aspire.Hosting; - +using Aspire.Hosting.Utils; using CommunityToolkit.Aspire.Testing; namespace CommunityToolkit.Aspire.Hosting.Logto.Tests; @@ -98,6 +98,19 @@ public void LogtoResourceDoesNotOverrideEntrypointByDefault() Assert.False(resource.TryGetAnnotationsOfType(out _)); } + [Fact] + public void AdminEndpointIsNotExportedAsAReferenceEndpoint() + { + var builder = DistributedApplication.CreateBuilder(); + var postgres = builder.AddPostgres("postgres"); + + var logto = builder.AddLogto("logto", postgres); + + var adminEndpoint = Assert.Single(logto.Resource.Annotations.OfType(), + endpoint => endpoint.Name == "admin"); + Assert.True(adminEndpoint.ExcludeReferenceEndpoint); + } + [Fact] public async Task LogtoResourceUsesOneShotDatabaseSetup() { @@ -139,4 +152,40 @@ public async Task LogtoDatabaseSetupCanDisablePwnedPasswordCheck() Assert.Equal(["-c", "npm run cli db seed -- --swe --dapc && npm run alteration deploy latest"], await setup.GetArgumentListAsync()); } -} + + [Fact] + public void LogtoDatabaseSetupIsIdempotent() + { + var builder = DistributedApplication.CreateBuilder(); + var postgres = builder.AddPostgres("postgres"); + + var logto = builder.AddLogto("logto", postgres); + logto.WithDatabaseSeeding(); + logto.WithDatabaseSeeding(); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + Assert.Single(appModel.Resources.OfType(), candidate => + candidate.Name == "logto-database-setup"); + Assert.Single(logto.Resource.Annotations.OfType(), wait => + wait.WaitType == WaitType.WaitForCompletion); + } + + [Fact] + public void LogtoDatabaseSetupIsNotAddedInPublishMode() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + var postgres = builder.AddPostgres("postgres"); + + var logto = builder.AddLogto("logto", postgres) + .WithDatabaseSeeding(); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + Assert.DoesNotContain(appModel.Resources, resource => resource.Name == "logto-database-setup"); + Assert.DoesNotContain(logto.Resource.Annotations.OfType(), wait => + wait.WaitType == WaitType.WaitForCompletion); + } +} \ No newline at end of file From 9610180bb1ad5d01206ffa4378048937e64c74a8 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 16 Jul 2026 02:38:18 +0300 Subject: [PATCH 39/40] Avoid Logto endpoint test deadlocks --- .../LogtoBuilderExtensions.cs | 23 +++++++---- .../LogtoResource.cs | 4 +- .../ResourceCreationTests.cs | 40 +------------------ 3 files changed, 19 insertions(+), 48 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs index d10e4dc3d..1ef030a93 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoBuilderExtensions.cs @@ -54,16 +54,23 @@ public static IResourceBuilder AddLogto( context.EnvironmentVariables.TryAdd("ENDPOINT", resource.PrimaryEndpoint.Url); } - var adminEndpoint = resource.GetEndpoint(LogtoResource.AdminEndpointName); - if (adminEndpoint.IsAllocated) + if (resource.AdminEndpointUrl is not null) { - context.EnvironmentVariables.TryAdd("ADMIN_ENDPOINT", GetCorsSafeLocalAdminUrl(adminEndpoint.Url)); + context.EnvironmentVariables.TryAdd("ADMIN_ENDPOINT", resource.AdminEndpointUrl); + } + else + { + var adminEndpoint = resource.GetEndpoint(LogtoResource.AdminEndpointName); + if (adminEndpoint.IsAllocated) + { + context.EnvironmentVariables.TryAdd("ADMIN_ENDPOINT", GetCorsSafeLocalAdminUrl(adminEndpoint.Url)); + } } }); if (builder.ExecutionContext.IsRunMode) { builderWithResource.WithUrlForEndpoint(LogtoResource.AdminEndpointName, url => - url.Url = GetCorsSafeLocalAdminUrl(url.Url)); + url.Url = resource.AdminEndpointUrl ?? GetCorsSafeLocalAdminUrl(url.Url)); } builderWithResource.WithDatabase(postgres, databaseName); SetHealthCheck(builder, builderWithResource, name); @@ -182,9 +189,9 @@ public static IResourceBuilder WithAdminEndpoint(this IResourceBu ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(url); - return builder - .WithEnvironment("ADMIN_ENDPOINT", url) - .WithUrlForEndpoint(LogtoResource.AdminEndpointName, annotation => annotation.Url = url); + builder.Resource.AdminEndpointUrl = url; + + return builder; } /// @@ -353,4 +360,4 @@ public static IResourceBuilder WithDatabaseSeeding( } } -#pragma warning restore ASPIREATS001 \ No newline at end of file +#pragma warning restore ASPIREATS001 diff --git a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs index deced7cc2..d7504de26 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs @@ -25,6 +25,8 @@ public sealed class LogtoResource(string name) internal PostgresServerResource? PostgresResource { get; set; } + internal string? AdminEndpointUrl { get; set; } + /// 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. @@ -78,4 +80,4 @@ IEnumerable> IResourceWithConnectionSt internal sealed class LogtoDatabaseSetupResource(string name) : ContainerResource(name); -#pragma warning restore ASPIREATS001 \ No newline at end of file +#pragma warning restore ASPIREATS001 diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs index 5bb60f44e..072d8c97e 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs @@ -1,47 +1,9 @@ using Aspire.Hosting; using Aspire.Hosting.Utils; -using CommunityToolkit.Aspire.Testing; - namespace CommunityToolkit.Aspire.Hosting.Logto.Tests; public class ResourceCreationTests { - [Fact] - public async Task LogtoResourceUsesAllocatedPublicEndpoints() - { - var builder = DistributedApplication.CreateBuilder(); - var postgres = builder.AddPostgres("postgres"); - - var logto = builder.AddLogto("logto", postgres) - .WithEndpoint("http", endpoint => - endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 49101)) - .WithEndpoint("admin", endpoint => - endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 49102)); - - var environment = await logto.Resource.GetEnvironmentVariablesAsync(); - - Assert.Equal("http://localhost:49101", environment["ENDPOINT"]); - Assert.Equal("http://127.0.0.1:49102", environment["ADMIN_ENDPOINT"]); - } - - [Fact] - public async Task ExplicitAdminEndpointOverridesAllocatedEndpoint() - { - var builder = DistributedApplication.CreateBuilder(); - var postgres = builder.AddPostgres("postgres"); - - var logto = builder.AddLogto("logto", postgres) - .WithEndpoint("http", endpoint => - endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 49101)) - .WithEndpoint("admin", endpoint => - endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 49102)) - .WithAdminEndpoint("https://admin.example.com"); - - var environment = await logto.Resource.GetEnvironmentVariablesAsync(); - - Assert.Equal("https://admin.example.com", environment["ADMIN_ENDPOINT"]); - } - [Fact] public void LogtoResourceGetsAdded() { @@ -188,4 +150,4 @@ public void LogtoDatabaseSetupIsNotAddedInPublishMode() Assert.DoesNotContain(logto.Resource.Annotations.OfType(), wait => wait.WaitType == WaitType.WaitForCompletion); } -} \ No newline at end of file +} From f6ccfb5a96e40954c122c51c2f1ebf0643397c4e Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 16 Jul 2026 02:55:23 +0300 Subject: [PATCH 40/40] Fix Logto test helper namespace --- .../ResourceCreationTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs index 072d8c97e..7573dafe3 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Logto.Tests/ResourceCreationTests.cs @@ -1,5 +1,7 @@ using Aspire.Hosting; using Aspire.Hosting.Utils; +using CommunityToolkit.Aspire.Testing; + namespace CommunityToolkit.Aspire.Hosting.Logto.Tests; public class ResourceCreationTests