Skip to content
Merged
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 AuthKit.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Project Path="src/Host/Host.csproj" />
<Folder Name="/Tests/">
<Project Path="tests/Host/AuthKit.Host.Tests.csproj" />
<Project Path="tests/Host.IntegrationTests/AuthKit.Host.IntegrationTests.csproj" />
<Project Path="tests/Plugins/Abstractions/AuthKit.Plugins.Abstractions.Tests.csproj" />
</Folder>
</Solution>
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />

<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.14.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageVersion Include="Scrutor" Version="6.1.0" />
<PackageVersion Include="System.CommandLine" Version="3.0.0-preview.7.26381.103" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="10.2.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.Swagger" Version="10.2.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.2.3" />
Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ COPY ["src/Plugins/Solutions/DevTools/DevTools.csproj", "src/Plugins/Solutions/D

COPY ["tests/Host/AuthKit.Host.Tests.csproj", "tests/Host/"]
COPY ["tests/Plugins/Abstractions/AuthKit.Plugins.Abstractions.Tests.csproj", "tests/Plugins/Abstractions/"]
COPY ["tools/PluginContractValidator/PluginContractValidator.csproj", "tools/PluginContractValidator/"]
COPY ["tools/AuthKit.PluginContractValidator/AuthKit.PluginContractValidator.csproj", "tools/AuthKit.PluginContractValidator/"]

RUN dotnet restore "AuthKit.slnx"

Expand All @@ -35,7 +35,8 @@ WORKDIR /src
RUN dotnet publish "src/Host/Host.csproj" -c Release -o /app/publish
RUN dotnet publish "src/Plugins/Solutions/DevTokens/DevTokens.csproj" -c Release -o /app/publish/plugins/DevTokens
RUN dotnet publish "src/Plugins/Solutions/DevTools/DevTools.csproj" -c Release -o /app/publish/plugins/DevTools
COPY src/Plugins/Solutions/DevTokens/plugin.manifest.json /app/publish/plugins/DevTokens/plugin.manifest.json
COPY src/Plugins/Solutions/DevTokens/manifest.json /app/publish/plugins/DevTokens/manifest.json
COPY src/Plugins/Solutions/DevTools/manifest.json /app/publish/plugins/DevTools/manifest.json

FROM base AS final
WORKDIR /app
Expand Down
62 changes: 62 additions & 0 deletions Docs/ADR/022-plugin-configuration-context-and-builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./021-swagger-serving-via-reflection.md) | [Next](./023-plugin-application-pipeline-hooks.md)

# [ADR-022] Extend Plugin Configuration With The Host Builder And Scoped Context

*2026-09* | Status: accepted

**Tag:** #adr_022

**Date:** 2026-09-12

**Scope:** AuthKit.Plugins.Abstractions + Host

## Context

Plugins previously configured services through `ConfigureServices(IServiceCollection, IConfiguration)`. That was sufficient for registrations, but it did not expose the actual host builder or a stable plugin-specific configuration context.

## Problem

Adding abstract members to `IAuthKitPlugin` would break existing plugins. Passing more individual host dependencies would also make the contract difficult to evolve and would encourage plugins to depend on host internals.

## Decision

The plugin contract exposes additive default interface members:

- `ConfigureServices(IHostApplicationBuilder, IConfiguration)` for plugins that need the real AuthKit host builder.
- `ConfigureServices(IServiceCollection, AuthKitPluginContext)` for plugins that need stable plugin identity and configuration context.
- The existing `ConfigureServices(IServiceCollection, IConfiguration)` remains valid for legacy plugins.

`AuthKitPluginContext` lives in the root `AuthKit.Plugins.Abstractions` namespace and exposes:

- stable `PluginId`;
- `PluginName`;
- plugin-scoped `Configuration` from `Plugins:{PluginId}` with a name fallback;
- read-only full `ApplicationConfiguration` for host-level settings.

The Host uses one dispatcher. It selects context configuration first, then host-builder configuration, then the legacy overload. Only one overload is invoked for a plugin, so compatibility paths cannot register the same services twice.

### Design Rationale

- Default interface implementations preserve source compatibility.
- The actual `WebApplicationBuilder` is passed instead of constructing an isolated builder.
- Plugin-scoped configuration prevents one plugin from accidentally reading another plugin's settings.
- The full application configuration remains available explicitly without creating a second DI or configuration system.

## Rejected

- Making the new overloads abstract would break existing plugins.
- Constructing a separate host builder would disconnect registrations from the running application.
- Passing `IServiceProvider` through the context would introduce service-locator behavior.
- Invoking every overload would cause duplicate registration and ambiguous behavior.

## Consequences

New plugins can opt into host-aware configuration or scoped context data. Existing plugins such as DevTokens and DevTools continue to use their legacy implementation without source changes. The dispatcher is a Host concern and the public contract remains independent of the Host's internal plugin loader.

## Related

- [ADR-009](./009-dynamic-plugin-discovery.md) - the plugin contract and dynamic discovery
- [ADR-019](./019-plugin-metadata-attribute.md) - declarative plugin identity used by the context
- [Issue #8](https://github.com/AuthKits/AuthKit.Server/issues/8) - host builder and plugin context requirements

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./021-swagger-serving-via-reflection.md) | [Next](./023-plugin-application-pipeline-hooks.md)
60 changes: 60 additions & 0 deletions Docs/ADR/023-plugin-application-pipeline-hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./022-plugin-configuration-context-and-builder.md) | [Next](./024-plugin-lifecycle-and-hosted-services.md)

# [ADR-023] Integrate Plugin Endpoints And Middleware Through Explicit Host Pipeline Hooks

*2026-09* | Status: accepted

**Tag:** #adr_023

**Date:** 2026-09-12

**Scope:** AuthKit.Plugins.Abstractions + Host

## Context

Plugins need to contribute endpoints and request middleware, but ASP.NET Core middleware ordering is part of application behavior. Discovery order, filesystem order, or assembly load order must not decide where plugin code runs.

## Problem

The original contract exposed only `MiddlewareType`, which provided one implicit middleware slot and no endpoint registration hook. Plugins could not explicitly place middleware relative to routing, authentication, authorization, or endpoint execution.

## Decision

The contract adds optional default hooks:

- `MapEndpoints(IEndpointRouteBuilder)` for normal ASP.NET Core endpoint routing;
- `ConfigureApplication(IApplicationBuilder)` for plugin application configuration;
- `ConfigurePipeline(IApplicationBuilder, PluginPipelinePosition)` for explicitly positioned middleware;
- `PipelinePosition`, using the strongly typed `PluginPipelinePosition` enum.

The supported positions are `BeforeRouting`, `AfterRouting`, `BeforeAuthentication`, `AfterAuthentication`, `BeforeAuthorization`, `AfterAuthorization`, `BeforeEndpoints`, and `AfterEndpoints`.

The Host applies hooks to the real application builder and endpoint route builder. Plugins at the same position are ordered by stable `Plugin.Id`, independently of discovery order. `AfterEndpoints` runs after REST and gRPC endpoint mapping.

Existing `MiddlewareType` behavior remains available in its original slot. If a plugin implements `ConfigureApplication` or `ConfigurePipeline`, the Host does not also register its `MiddlewareType`, preventing accidental duplicate middleware registration. Hook exceptions and invalid positions are surfaced explicitly.

### Design Rationale

- Endpoint hooks use the normal ASP.NET Core routing system, preserving endpoint metadata, authorization, authentication, OpenAPI discovery, and endpoint selection.
- A finite enum exposes meaningful pipeline stages without making every internal middleware implementation a public dependency.
- Sorting by stable plugin ID makes equal-position ordering deterministic and testable.
- Default interface members keep plugins that only use `MiddlewareType` source-compatible.

## Rejected

- Keeping middleware order equal to plugin discovery order is nondeterministic.
- Arbitrary string positions are weakly typed and cannot be validated reliably.
- A parallel endpoint router would bypass ASP.NET Core endpoint metadata and selection.
- Silently moving invalid positions to the end would hide plugin configuration errors.

## Consequences

Plugins can participate in the host's endpoint and middleware pipeline without modifying host startup code. Pipeline placement is explicit and reviewable. Plugin authors must choose a supported stage when using `ConfigurePipeline`; the Host owns the stage boundaries and deterministic ordering.

## Related

- [ADR-009](./009-dynamic-plugin-discovery.md) - plugin discovery and contract boundary
- [ADR-010](./010-plugin-loading-from-directory.md) - plugin loading and legacy middleware slot
- [Issue #9](https://github.com/AuthKits/AuthKit.Server/issues/9) - endpoint and application pipeline requirements

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./022-plugin-configuration-context-and-builder.md) | [Next](./024-plugin-lifecycle-and-hosted-services.md)
62 changes: 62 additions & 0 deletions Docs/ADR/024-plugin-lifecycle-and-hosted-services.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./023-plugin-application-pipeline-hooks.md) | [Next]()

# [ADR-024] Bridge Plugin Lifecycle Hooks To The Standard .NET Host Lifecycle

*2026-09* | Status: accepted

**Tag:** #adr_024

**Date:** 2026-09-12

**Scope:** AuthKit.Plugins.Abstractions + Host

## Context

Plugins may need to initialize runtime resources, perform work after startup, release external registrations during shutdown, or contribute background services. Static service registration cannot represent those operations safely.

## Problem

Without explicit lifecycle hooks, plugins would need host-specific startup code or custom hosted-service schedulers. Manually invoking plugin background services would also bypass the standard .NET host lifecycle and its cancellation semantics.

## Decision

`IAuthKitPlugin` exposes additive default members:

- `OnStartingAsync(CancellationToken)`;
- `OnStartedAsync(CancellationToken)`;
- `OnStoppingAsync(CancellationToken)`;
- `GetHostedServices()` returning a non-null `IReadOnlyList<IHostedService>`.

The Host uses one `PluginLifecycleHostedService` bridge registered through the normal DI container. It orders plugins by stable `Plugin.Id`:

- `OnStartingAsync` runs in ascending order during hosted-service startup;
- `OnStartedAsync` runs after `ApplicationStarted` and only after successful startup;
- `OnStoppingAsync` runs in reverse order when `ApplicationStopping` is signaled.

Plugin-provided hosted services are registered as singleton `IHostedService` instances before host startup. Their `StartAsync` and `StopAsync` methods are therefore invoked by the standard .NET host rather than by AuthKit code. Null results, null service instances, duplicate registration, and lifecycle exceptions are rejected explicitly. Lifecycle failures include the plugin ID and lifecycle stage in the thrown exception.

### Design Rationale

- Standard `IHostedService` integration preserves the framework's startup, shutdown, cancellation, and disposal behavior.
- One lifecycle bridge prevents duplicate hook invocation and avoids a custom scheduler.
- Stable ID ordering makes startup and shutdown deterministic regardless of discovery order.
- Default interface members preserve compatibility for plugins that do not need lifecycle behavior.

## Rejected

- Calling hosted-service `StartAsync` and `StopAsync` manually would create a second lifecycle implementation.
- Creating another service provider or service scope would split plugin dependencies from the application DI container.
- Ignoring lifecycle exceptions would allow the host to report a plugin as healthy when initialization failed.
- Using discovery order would make lifecycle behavior depend on filesystem or assembly enumeration.

## Consequences

Plugin lifecycle failures fail explicitly through the host startup/shutdown path. Plugin authors can use cancellation-aware hooks for initialization and cleanup, while long-running work belongs in `IHostedService` implementations returned by `GetHostedServices()`. Existing plugins that return no hosted services and implement no hooks remain valid through safe defaults.

## Related

- [ADR-009](./009-dynamic-plugin-discovery.md) - plugin discovery and contract boundary
- [ADR-016](./016-marten-and-wolverine-infrastructure.md) - host infrastructure lifecycle
- [Issue #10](https://github.com/AuthKits/AuthKit.Server/issues/10) - lifecycle and hosted-service requirements

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./023-plugin-application-pipeline-hooks.md) | [Next]()
9 changes: 9 additions & 0 deletions Docs/ADR/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ The table below shows the architecture areas and their current scope.
| [ADR-014](./014-error-responses-via-middleware.md) | Render HTTP Errors As RFC 7807 Problem Details Via Middleware | Host | accepted | 2026-08-26 |
| [ADR-015](./015-keycloak-external-jwt-authority.md) | Use Keycloak As The External JWT Authority | Host | accepted | 2026-08-26 |
| [ADR-016](./016-marten-and-wolverine-infrastructure.md) | Use Marten And Wolverine As Host Infrastructure | Host | accepted | 2026-08-26 |
| [ADR-017](./017-Plugin-Contract-and-Dynamic-Loading-Architecture.md) | Define The Plugin Contract And Dynamic Loading Architecture | Plugins | accepted | 2026-09-11 |
| [ADR-017](./017-api-key-credential-extraction-strategies.md) | Define API Key Credential Extraction Strategies | Host | accepted | 2026-09-11 |
| [ADR-018](./018-security-scheme-contract-explicit-handling.md) | Handle Security Scheme Contract Values Explicitly | Plugins | accepted | 2026-09-11 |
| [ADR-019](./019-plugin-metadata-attribute.md) | Declare Plugin Identity Through The PluginMetadata Attribute | Plugins | accepted | 2026-09-11 |
| [ADR-020](./020-devtools-plugin.md) | Host Developer Tools Through A Dedicated DevTools Plugin | Plugins | accepted | 2026-09-11 |
| [ADR-021](./021-swagger-serving-via-reflection.md) | Serve Swagger Through Reflection-Based SwaggerHost In DevTools | Plugins | accepted | 2026-09-11 |
| [ADR-022](./022-plugin-configuration-context-and-builder.md) | Extend Plugin Configuration With The Host Builder And Scoped Context | Plugins | accepted | 2026-09-12 |
| [ADR-023](./023-plugin-application-pipeline-hooks.md) | Integrate Plugin Endpoints And Middleware Through Explicit Host Pipeline Hooks | Plugins | accepted | 2026-09-12 |
| [ADR-024](./024-plugin-lifecycle-and-hosted-services.md) | Bridge Plugin Lifecycle Hooks To The Standard .NET Host Lifecycle | Plugins | accepted | 2026-09-12 |

## Relationships Between Areas

Expand Down
26 changes: 17 additions & 9 deletions src/Host/Configuration/AppMiddlewareConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Host.Plugins;
using Host.Restful.Middleware.Exceptions;
using Host.Security.Middleware;
using AuthKit.Plugins.Abstractions;
using AuthKit.Plugins.Abstractions.Contracts;

namespace Host.Configuration;

Expand All @@ -14,9 +16,11 @@ namespace Host.Configuration;
/// middleware, and authentication and authorization.
/// </para>
/// <para>
/// Plugin middleware is inserted after the host exception handling middleware
/// and before authentication so plugins can participate in request processing
/// before the authenticated endpoint pipeline is reached.
/// New pipeline hooks are inserted at their strongly typed
/// <see cref="PluginPipelinePosition"/>. Plugins at the same position are
/// ordered by stable plugin ID. Legacy <see cref="IAuthKitPlugin.MiddlewareType"/>
/// middleware remains in its original slot unless the plugin opts into a new
/// application or pipeline hook.
/// </para>
/// </remarks>
public static class AppMiddlewareConfiguration
Expand All @@ -26,28 +30,32 @@ public static class AppMiddlewareConfiguration
/// </summary>
/// <param name="plugins">
/// The plugins loaded during application startup. Plugins may optionally
/// contribute middleware through their configured middleware type.
/// contribute middleware, application hooks, or positioned pipeline hooks.
/// </param>
/// <returns>The configured <see cref="WebApplication"/> instance.</returns>
public static WebApplication ConfigureMiddleware(
this WebApplication app,
IReadOnlyList<LoadedPlugin> plugins)
{
PluginApplicationConfiguration.ConfigureApplications(app, plugins);
PluginApplicationConfiguration.ConfigurePipeline(app, plugins, PluginPipelinePosition.BeforeRouting);
app.UseRouting();
PluginApplicationConfiguration.ConfigurePipeline(app, plugins, PluginPipelinePosition.AfterRouting);

app.UseMiddleware<ValidationExceptionMiddleware>();
app.UseMiddleware<ExceptionHandlingMiddleware>();

foreach (var plugin in plugins)
{
if (plugin.Plugin.MiddlewareType is { } middlewareType)
app.UseMiddleware(middlewareType);
}
PluginApplicationConfiguration.ConfigureLegacyMiddleware(app, plugins);
PluginApplicationConfiguration.ConfigurePipeline(app, plugins, PluginPipelinePosition.BeforeAuthentication);

app.UseMiddleware<ApiKeyCredentialExtractor>();

app.UseAuthentication();
PluginApplicationConfiguration.ConfigurePipeline(app, plugins, PluginPipelinePosition.AfterAuthentication);
PluginApplicationConfiguration.ConfigurePipeline(app, plugins, PluginPipelinePosition.BeforeAuthorization);
app.UseAuthorization();
PluginApplicationConfiguration.ConfigurePipeline(app, plugins, PluginPipelinePosition.AfterAuthorization);
PluginApplicationConfiguration.ConfigurePipeline(app, plugins, PluginPipelinePosition.BeforeEndpoints);

return app;
}
Expand Down
10 changes: 9 additions & 1 deletion src/Host/Configuration/EndpointConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,15 @@ public static class EndpointConfiguration
/// <summary>
/// Maps AuthKit application endpoints to the specified web application.
/// </summary>
/// <param name="app"></param>
/// <param name="plugins">
/// The plugins whose endpoint hooks are invoked after host endpoint services
/// are configured and before request processing starts.
/// </param>
/// <returns>The configured <see cref="WebApplication"/> instance.</returns>
public static WebApplication MapAppEndpoints(this WebApplication app)
public static WebApplication MapAppEndpoints(
this WebApplication app,
IReadOnlyList<LoadedPlugin> plugins)
{
app.MapGet("/", () => Results.Json(new
{
Expand All @@ -46,6 +53,7 @@ public static WebApplication MapAppEndpoints(this WebApplication app)
environment = app.Environment.EnvironmentName
}));
app.MapControllers();
PluginApplicationConfiguration.MapEndpoints(app, plugins);

app.MapGet("/health", async (HttpContext context, IJwtKeyStore keyStore, IReadOnlyList<LoadedPlugin> plugins) =>
{
Expand Down
Loading
Loading