Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
cd63a59
Add Logto with PostgreSQL and Redis integration (#815)
axies20 Nov 30, 2025
0daa9ae
Add Logto client hosting support (#817)
axies20 Dec 1, 2025
34b16e2
Add Logto Client API example project
axies20 Dec 2, 2025
9c725dd
Add tests and enhancements for Logto Client integration
axies20 Dec 3, 2025
60f73e9
Refactor Logto client integration
axies20 Dec 6, 2025
8a0f9de
Remove unused `/signout` route from ClientApi example
axies20 Dec 6, 2025
95d01da
Rename `ClientApi` example to `ClientOIDC` and update method signatures
axies20 Dec 7, 2025
541111c
Add `ClientJWT` example with Logto JWT authentication support
axies20 Dec 7, 2025
bc8b00c
Merge branch 'main' into Logto
axies20 Mar 27, 2026
d4378b3
Update examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/C…
axies20 Mar 30, 2026
4557bf8
Update src/CommunityToolkit.Aspire.Hosting.Logto.Client/CommunityTool…
axies20 Mar 30, 2026
0e3f42c
Merge branch 'main' into Logto
axies20 Mar 30, 2026
3976aec
Remove unused package references and resolve merge conflict in projec…
axies20 Mar 30, 2026
6685b60
Update package versions and project SDKs for Logto client integration
axies20 Mar 30, 2026
cde0cbd
Update `ClientJWT` example to use a constant for API audience in Logt…
axies20 Mar 30, 2026
d271f0a
Remove `<IsPreview>` property from Logto project
axies20 Mar 30, 2026
f282231
Rename `Hosting.Logto.Client` to `Logto.Client` and adjust related re…
axies20 Mar 31, 2026
a2e300c
Add `WithDeprecationTracing` method to enable Node.js deprecation tra…
axies20 Mar 31, 2026
97ce6d6
Merge branch 'main' into Logto
axies20 Apr 2, 2026
63356b8
Add `Hosting.Logto.Tests` and `Logto.Client.Tests` to test matrix in …
axies20 Apr 2, 2026
20f300b
Refactor Logto configuration: introduce validation for Logto options …
axies20 Apr 15, 2026
cbd16a8
Refactor `AppHostTest` to use `AspireIntegrationTestFixture`; add `Ap…
axies20 Apr 15, 2026
907c4d5
Allow null `port` and `adminPort` in `WithResourcePort` to enable ran…
axies20 Apr 15, 2026
bac366e
Update Logto container tag to v1.38
axies20 Apr 17, 2026
95c6f2c
Refactor `LogtoContainerResource` to `LogtoResource` and update relat…
axies20 Apr 17, 2026
231ae98
Set default HTTP and admin ports in `WithResourcePort` method of `Log…
axies20 Apr 17, 2026
6bfd3ca
Merge branch 'main' into Logto
axies20 May 12, 2026
9aa53f9
Merge branch 'main' into Logto
aaronpowell May 18, 2026
dcbb327
Merge branch 'main' into Logto
aaronpowell May 21, 2026
99a5a14
Merge branch 'main' into Logto
aaronpowell May 25, 2026
3b00722
Merge branch 'main' into Logto
axies20 May 25, 2026
d67f854
Update examples/logto/CommunityToolkit.Aspire.Hosting.Logto.AppHost/C…
aaronpowell May 25, 2026
614b9e9
Merge branch 'main' into Logto
axies20 Jun 5, 2026
6311c8b
Add Logto integration: introduce `LogtoResource` and client builders …
axies20 Jun 5, 2026
6be30bf
Merge branch 'main' into Logto
aaronpowell Jun 11, 2026
dcd379e
Merge branch 'main' into Logto
aaronpowell Jun 15, 2026
98603e5
Apply suggestion from @aaronpowell
aaronpowell Jun 15, 2026
dc8e10c
Fixing build errors
aaronpowell Jun 15, 2026
c7eb02e
Fix Logto test startup
aaronpowell Jun 15, 2026
f4476a6
not using persisted db
aaronpowell Jun 17, 2026
0284f21
Testing a better endpoint
aaronpowell Jun 17, 2026
1c631eb
deleting the generated files
aaronpowell Jun 17, 2026
ec3e661
Adding missing README.md
aaronpowell Jun 17, 2026
cf368fc
Merge branch 'Logto' of https://github.com/axies20/CommunityToolkit.A…
aaronpowell Jun 17, 2026
8e2dc53
Add unit tests for `AddLogtoJwtBearer` to validate configuration and …
axies20 Jul 15, 2026
303257c
Deprecate `WithSensitiveUsername(...)` in favor of tenant-level usern…
axies20 Jul 15, 2026
c2003ca
Add database seeding and upgrade support for Logto with optional pwne…
axies20 Jul 15, 2026
a780124
Ensure Logto options are fully preserved in OIDC setup; validate endp…
axies20 Jul 15, 2026
432743a
Add support for CORS-safe local Admin Console URLs in Logto; update e…
axies20 Jul 15, 2026
27b327a
Merge branch 'main' of https://github.com/CommunityToolkit/Aspire int…
axies20 Jul 15, 2026
b86cd02
Align Logto hosting integration with toolkit conventions
axies20 Jul 15, 2026
9610180
Avoid Logto endpoint test deadlocks
axies20 Jul 15, 2026
f6ccfb5
Fix Logto test helper namespace
axies20 Jul 15, 2026
51f51f9
Merge branch 'main' into Logto
axies20 Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<PropertyGroup>
<Description>.NET Aspire hosting extensions for Logto (includes PostgreSQL and Redis integration).</Description>
<AdditionalPackageTags>logto redis postgres hosting extensions</AdditionalPackageTags>
<EnableAspireIntegrationAnalyzers>true</EnableAspireIntegrationAnalyzers>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;
using System.ComponentModel;

#pragma warning disable ASPIREATS001 // AspireExport is experimental

Expand All @@ -25,7 +26,7 @@ public static class LogtoBuilderExtensions
[AspireExport]
public static IResourceBuilder<LogtoResource> AddLogto(
this IDistributedApplicationBuilder builder,
string name,
[ResourceName] string name,
IResourceBuilder<PostgresServerResource> postgres,
string databaseName = "logto_db",
int? port = null,
Expand All @@ -44,12 +45,52 @@ public static IResourceBuilder<LogtoResource> 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);
}

if (resource.AdminEndpointUrl is not null)
{
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 = resource.AdminEndpointUrl ?? 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);
}

/// <summary>
/// Enables Node.js deprecation tracing for the Logto by setting the
/// NODE_OPTIONS environment variable to '--trace-deprecation'.
Expand Down Expand Up @@ -131,7 +172,8 @@ public static IResourceBuilder<LogtoResource> WithResourcePort(
.WithHttpEndpoint(
port: adminPort,
targetPort: LogtoResource.DefaultHttpAdminPort,
name: LogtoResource.AdminEndpointName);
name: LogtoResource.AdminEndpointName)
.WithEndpoint(LogtoResource.AdminEndpointName, endpoint => endpoint.ExcludeReferenceEndpoint = true);
}

/// <summary>
Expand All @@ -147,7 +189,9 @@ public static IResourceBuilder<LogtoResource> WithAdminEndpoint(this IResourceBu
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(url);

return builder.WithEnvironment("ADMIN_ENDPOINT", url);
builder.Resource.AdminEndpointUrl = url;

return builder;
}

/// <summary>
Expand Down Expand Up @@ -184,7 +228,9 @@ public static IResourceBuilder<LogtoResource> WithTrustProxyHeader(this IResourc
/// <param name="builder">The resource builder for the Logto resource to configure.</param>
/// <param name="sensitiveUsername">A value indicating whether usernames should be treated as case-sensitive.</param>
/// <returns>The updated resource builder with the configured case-sensitivity setting.</returns>
[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<LogtoResource> WithSensitiveUsername(this IResourceBuilder<LogtoResource> builder, bool sensitiveUsername)
{
ArgumentNullException.ThrowIfNull(builder);
Expand Down Expand Up @@ -259,23 +305,58 @@ public static IResourceBuilder<LogtoResource> 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);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="builder">The resource builder for the Logto resource to configure.</param>
/// <param name="disableAdminPwnedPasswordCheck">
/// Disables the Have I Been Pwned check for the initial admin password. Use this only in air-gapped environments.
/// </param>
/// <returns>The resource builder for the configured Logto resource.</returns>
[AspireExport]
public static IResourceBuilder<LogtoResource> WithDatabaseSeeding(this IResourceBuilder<LogtoResource> builder)
public static IResourceBuilder<LogtoResource> WithDatabaseSeeding(
this IResourceBuilder<LogtoResource> builder,
bool disableAdminPwnedPasswordCheck = false)
{
ArgumentNullException.ThrowIfNull(builder);

return 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<LogtoDatabaseSetupResource>(setupName, out _))
{
return builder;
}

var seedArguments = disableAdminPwnedPasswordCheck ? "--swe --dapc" : "--swe";

var setup = builder.ApplicationBuilder
.AddResource(new LogtoDatabaseSetupResource(setupName))
.WithImage(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);
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/CommunityToolkit.Aspire.Hosting.Logto/LogtoResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public sealed class LogtoResource(string name)

private EndpointReference? _primaryEndpointReference;

internal ReferenceExpression? DatabaseUrl { get; set; }

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.
Expand Down Expand Up @@ -72,4 +78,6 @@ IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionSt
}
}

internal sealed class LogtoDatabaseSetupResource(string name) : ContainerResource(name);

#pragma warning restore ASPIREATS001
4 changes: 2 additions & 2 deletions src/CommunityToolkit.Aspire.Hosting.Logto/LogtoTags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ internal sealed class LogtoContainerImageTags
/// <summary>svhd/logto</summary>
public const string Image = "svhd/logto";

/// <summary>1.39</summary>
public const string Tag = "1.39";
/// <summary>1.41</summary>
public const string Tag = "1.41";
}
113 changes: 64 additions & 49 deletions src/CommunityToolkit.Aspire.Hosting.Logto/README.md
Original file line number Diff line number Diff line change
@@ -1,67 +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:
- `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<Projects.MyApi>("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) // optional override, default is already true
.WithSensitiveUsername(true)
.WithNodeEnv("production");
builder.Build().Run();
```

The same integration is exported to a TypeScript AppHost:

```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);
```

Logto will be configured with:
`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.

### Database setup

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.

For an air-gapped environment, use `WithDatabaseSeeding(disableAdminPwnedPasswordCheck: true)` to pass Logto's `--dapc` option during initial administrator creation.

### 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`.

## Connection properties

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}`.

| 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` |

The Admin Console endpoint is intentionally excluded from service-reference endpoint expansion; consumers receive the public Logto endpoint only.

* `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`.
## 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 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.
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).
25 changes: 22 additions & 3 deletions src/CommunityToolkit.Aspire.Logto.Client/LogtoClientBuilder.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<OpenIdConnectOptions>(authenticationScheme, opt =>
{
Expand Down Expand Up @@ -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.");
Expand Down Expand Up @@ -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.");
}
}
}
Loading
Loading