Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ jobs:
Hosting.Sqlite.Tests,
Hosting.SqlServer.Extensions.Tests,
Hosting.SurrealDb.Tests,
Hosting.Zitadel.Tests,

# Client integration tests
GoFeatureFlag.Tests,
Expand Down
5 changes: 5 additions & 0 deletions CommunityToolkit.Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@
<Project Path="examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost.csproj" />
<Project Path="examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ServiceDefaults/CommunityToolkit.Aspire.Hosting.SurrealDb.ServiceDefaults.csproj" />
</Folder>
<Folder Name="/examples/zitadel/">
<Project Path="examples\zitadel\CommunityToolkit.Aspire.Hosting.Zitadel.AppHost\CommunityToolkit.Aspire.Hosting.Zitadel.AppHost.csproj" />
</Folder>
<Folder Name="/src/">
<Project Path="src/CommunityToolkit.Aspire.GoFeatureFlag/CommunityToolkit.Aspire.GoFeatureFlag.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.ActiveMQ/CommunityToolkit.Aspire.Hosting.ActiveMQ.csproj" />
Expand Down Expand Up @@ -199,6 +202,7 @@
<Project Path="src/CommunityToolkit.Aspire.Hosting.Sqlite/CommunityToolkit.Aspire.Hosting.Sqlite.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.SurrealDb/CommunityToolkit.Aspire.Hosting.SurrealDb.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.csproj" />
<Project Path="src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj" />
<Project Path="src/CommunityToolkit.Aspire.MassTransit.RabbitMQ/CommunityToolkit.Aspire.MassTransit.RabbitMQ.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Meilisearch/CommunityToolkit.Aspire.Meilisearch.csproj" />
Expand Down Expand Up @@ -251,6 +255,7 @@
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Sqlite.Tests/CommunityToolkit.Aspire.Hosting.Sqlite.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.KurrentDB.Tests/CommunityToolkit.Aspire.KurrentDB.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Keycloak.Extensions.Tests/CommunityToolkit.Aspire.Hosting.Keycloak.Extensions.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.MassTransit.RabbitMQ.Tests/CommunityToolkit.Aspire.MassTransit.RabbitMQ.Tests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
var builder = DistributedApplication.CreateBuilder(args);

var database = builder.AddPostgres("postgres");

builder.AddZitadel("zitadel")
.WithDatabase(database);

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Aspire.AppHost.Sdk/13.0.0">

<PropertyGroup>
<OutputType>Exe</OutputType>
<UserSecretsId>981579ce-9426-4ad6-b6f1-192f3f4cf73b</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\CommunityToolkit.Aspire.Hosting.Zitadel\CommunityToolkit.Aspire.Hosting.Zitadel.csproj" IsAspireProjectResource="false" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17100;http://localhost:15025",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21182",
"ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23175",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22260"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15025",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19275",
"ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18212",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20294"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>A .NET Aspire host integration for Zitadel.</Description>
<AdditionalPackageTags>zitadel auth identity oauth2 authenticatioon openid-connect oidc</AdditionalPackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" />
<PackageReference Include="Aspire.Hosting.PostgreSQL" />
</ItemGroup>

</Project>
65 changes: 65 additions & 0 deletions src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# CommunityToolkit.Aspire.Hosting.Zitadel library

Provides extension methods and resource definitions for a .NET Aspire AppHost to configure Zitadel.

## Getting Started

### Install the package

In your AppHost project, install the package using the following command:

```dotnetcli
dotnet add package CommunityToolkit.Aspire.Hosting.Zitadel
```

### Example usage

Then, in the _Program.cs_ file of `AppHost`, define a Zitadel resource, then call `AddZitadel`:

```csharp
builder.AddZitadel("zitadel");
```

Zitadel *requires* a Postgres database, you can add one with `WithDatabase`:
```csharp
var database = builder.AddPostgres("postgres");

builder.AddZitadel("zitadel")
.WithDatabase(database);
```
You can also pass in a database rather than server (`AddPostgres().AddDatabase()`).

### Configuring the External Domain

By default, Zitadel uses `{name}.dev.localhost` as the external domain, which works well for local development. For production deployments or custom scenarios, you can configure a custom external domain:

**Option 1: Using the parameter**
```csharp
builder.AddZitadel("zitadel", externalDomain: "auth.example.com");
```

**Option 2: Using the fluent API**
```csharp
builder.AddZitadel("zitadel")
.WithExternalDomain("auth.example.com");
```

**Option 3: From configuration**
```csharp
var domain = builder.Configuration["Zitadel:ExternalDomain"];
builder.AddZitadel("zitadel", externalDomain: domain);
```

#### Why `.dev.localhost`?

`.dev.localhost` is a special top-level domain that:
- Automatically resolves to `127.0.0.1` without requiring DNS configuration
- Provides unique subdomains for each Zitadel instance (e.g., `zitadel1.dev.localhost`, `zitadel2.dev.localhost`)
- Works reliably in local development and CI/CD environments
- Satisfies Zitadel's requirement for stable hostnames in OIDC/OAuth2 flows

For production deployments, replace this with your actual domain name using one of the configuration methods above.

## Feedback & contributing

https://github.com/CommunityToolkit/Aspire
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace CommunityToolkit.Aspire.Hosting.Zitadel;

internal static class ZitadelContainerImageTags
{
/// <remarks>Github Container Registry</remarks>
public const string Registry = "ghcr.io";

/// <remarks>zitadel/zitadel</remarks>
public const string Image = "zitadel/zitadel";

/// <remarks>v4.7.0</remarks>
public const string Tag = "v4.7.0";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Hosting.Zitadel;

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for adding Zitadel to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static class ZitadelHostingExtensions
{
/// <summary>
/// Adds a Zitadel container resource to the <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder" /> to add the Zitadel container to.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="port">The host port used when launching the container. If <c>null</c> a random port will be assigned</param>
/// <param name="username">An optional parameter to set a username for the admin account, if <c>null</c> will auto generate one.</param>
/// <param name="password">An optional parameter to set a password for the admin account, if <c>null</c> will auto generate one.</param>
/// <param name="masterKey">An optional parameter to set the masterkey, if <c>null</c> will auto generate one.</param>
/// <param name="externalDomain">The external domain for Zitadel. Defaults to <c>{name}.dev.localhost</c> which works for local development. For production deployments, specify the actual domain (e.g., "auth.example.com").</param>
public static IResourceBuilder<ZitadelResource> AddZitadel(
this IDistributedApplicationBuilder builder,
[ResourceName] string name,
int? port = null,
IResourceBuilder<ParameterResource>? username = null,
IResourceBuilder<ParameterResource>? password = null,
IResourceBuilder<ParameterResource>? masterKey = null,
string? externalDomain = null
)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);

// Use provided external domain or default to {name}.dev.localhost
var domain = externalDomain ?? $"{name}.dev.localhost";

var usernameParameter = username?.Resource ?? new ParameterResource($"{name}-username", _ => "admin", false);
var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", minSpecial: 1);
var masterKeyParameter = masterKey?.Resource ?? ParameterResourceBuilderExtensions.CreateGeneratedParameter(builder, $"{name}-masterKey", true, new GenerateParameterDefault
{
MinLength = 32, // Zitadel requires 32, CreateDefaultPasswordParameter generates 22
Lower = true,
Upper = true,
Numeric = true,
Special = true,
MinLower = 1,
MinUpper = 1,
MinNumeric = 1,
MinSpecial = 1
});

var resource = new ZitadelResource(name)
{
AdminUsernameParameter = usernameParameter,
AdminPasswordParameter = passwordParameter
};

var zitadelBuilder = builder.AddResource(resource)
.WithImage(ZitadelContainerImageTags.Image)
.WithImageTag(ZitadelContainerImageTags.Tag)
.WithImageRegistry(ZitadelContainerImageTags.Registry)
.WithArgs("start-from-init", "--masterkeyFromEnv")
.WithHttpEndpoint(
targetPort: 8080,
port: port,
name: ZitadelResource.HttpEndpointName
)
.WithHttpHealthCheck("/healthz")
.WithEnvironment("ZITADEL_MASTERKEY", masterKeyParameter)
.WithEnvironment("ZITADEL_TLS_ENABLED", "false")
.WithEnvironment("ZITADEL_EXTERNALSECURE", "false")
.WithEnvironment("ZITADEL_EXTERNALDOMAIN", domain)
.WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard");

// Use ReferenceExpression for the port to avoid issues with endpoint allocation
var endpoint = resource.GetEndpoint(ZitadelResource.HttpEndpointName);
var portExpression = ReferenceExpression.Create($"{endpoint.Property(EndpointProperty.Port)}");

return zitadelBuilder
.WithEnvironment("ZITADEL_EXTERNALPORT", portExpression)
// Disable Login V2 for simpler setup (no separate login container needed)
.WithEnvironment("ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED", "false")
// Configure admin user
.WithEnvironment("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME", usernameParameter)
.WithEnvironment("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD", passwordParameter)
.WithEnvironment("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED", "false");
}

/// <summary>
/// Adds database support to the Zitadel resource.
/// </summary>
/// <param name="builder">The Zitadel resource to add database support to.</param>
/// <param name="server">The Postgres server resource to use for the database.</param>
/// <param name="databaseName">An optional name for the database Zitadel will use, if left empty will default to <c>"zitadel-db"</c>.</param>
public static IResourceBuilder<ZitadelResource> WithDatabase(
this IResourceBuilder<ZitadelResource> builder,
IResourceBuilder<PostgresServerResource> server,
[ResourceName] string? databaseName = null
)
{
databaseName = string.IsNullOrWhiteSpace(databaseName) ? "zitadel-db" : databaseName;
var database = server.AddDatabase(databaseName);

return WithDatabase(builder, database);
}

/// <summary>
/// Adds database support to the Zitadel resource.
/// </summary>
/// <param name="builder">The Zitadel resource to add database support to.</param>
/// <param name="database">The Postgres database resource to use for the database.</param>
public static IResourceBuilder<ZitadelResource> WithDatabase(this IResourceBuilder<ZitadelResource> builder, IResourceBuilder<PostgresDatabaseResource> database)
{
ArgumentNullException.ThrowIfNull(database);

builder
.WithEnvironment("ZITADEL_DATABASE_POSTGRES_USER_USERNAME", database.Resource.Parent.UserNameReference)
.WithEnvironment("ZITADEL_DATABASE_POSTGRES_USER_PASSWORD", database.Resource.Parent.PasswordParameter)
.WithEnvironment("ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME", database.Resource.Parent.UserNameReference)
.WithEnvironment("ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD", database.Resource.Parent.PasswordParameter)
.WithEnvironment("ZITADEL_DATABASE_POSTGRES_HOST", database.Resource.Parent.Host)
.WithEnvironment("ZITADEL_DATABASE_POSTGRES_PORT", database.Resource.Parent.Port)
.WithEnvironment("ZITADEL_DATABASE_POSTGRES_DATABASE", database.Resource.DatabaseName)
.WithReference(database)
.WaitFor(database);

return builder;
}

/// <summary>
/// Configures the external domain for the Zitadel resource. This overrides the default domain set in <see cref="AddZitadel"/>.
/// </summary>
/// <param name="builder">The Zitadel resource builder.</param>
/// <param name="externalDomain">The external domain to use (e.g., "auth.example.com"). Cannot be null or empty.</param>
/// <returns>The resource builder for chaining.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="externalDomain"/> is null or whitespace.</exception>
public static IResourceBuilder<ZitadelResource> WithExternalDomain(
this IResourceBuilder<ZitadelResource> builder,
string externalDomain)
{
ArgumentException.ThrowIfNullOrWhiteSpace(externalDomain);

return builder.WithEnvironment("ZITADEL_EXTERNALDOMAIN", externalDomain);
}
}
21 changes: 21 additions & 0 deletions src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Aspire.Hosting.ApplicationModel;

namespace CommunityToolkit.Aspire.Hosting.Zitadel;

/// <summary>
/// Resource for the Zitadel API server.
/// </summary>
public sealed class ZitadelResource(string name) : ContainerResource(name)
{
internal const string HttpEndpointName = "http";

/// <summary>
/// The parameter that contains the (default) Zitadel admin username.
/// </summary>
public required ParameterResource AdminUsernameParameter { get; set; }

/// <summary>
/// The parameter that contains the (default) Zitadel admin password.
/// </summary>
public required ParameterResource AdminPasswordParameter { get; set; }
}
Loading