Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
15ff2b9
refactor(plugin): add pipeline hook registration
rian-be Sep 12, 2026
c944b63
refactor(plugin): introduce lifecycle and configuration hooks
rian-be Sep 12, 2026
e3263e9
feat(plugin): introduce plugin context and pipeline positions
rian-be Sep 12, 2026
5326187
test(integration): add plugin loading and lifecycle tests
rian-be Sep 12, 2026
4a6a3a1
test(plugin): add host lifecycle and configuration tests
rian-be Sep 12, 2026
518ea49
feat(plugin): introduce lifecycle and service configuration infrastru…
rian-be Sep 12, 2026
c9761aa
docs(adr): document plugin configuration, pipeline, and lifecycle hooks
rian-be Sep 12, 2026
faa339d
chore(plugin): wire integration tests into the solution and fix manif…
rian-be Sep 12, 2026
f69edeb
refactor(test): prefer Path.Join over Path.Combine for plugin staging…
rian-be Sep 12, 2026
3099850
feat(host): integrate plugin configuration hooks
rian-be Sep 12, 2026
b294a2f
feat(plugin): integrate plugins with host configuration
rian-be Sep 12, 2026
984b3ec
feat(plugin): introduce auth and options integration hooks
rian-be Sep 12, 2026
2ef209d
docs(adr): add ADR-025 and ADR-026 for plugin integrations
rian-be Sep 12, 2026
af3c67d
fix(plugin): remove shared static cache from IAuthKitPlugin capabilities
rian-be Sep 12, 2026
e3e55b0
fix(auth): guard against plugin authorization policy collisions
rian-be Sep 12, 2026
1528de8
docs(plugin): document integration hooks
rian-be Sep 12, 2026
56695e3
test(plugin): cover configuration scoping and auth collisions
rian-be Sep 12, 2026
6d17f90
refactor(plugin): centralize configuration and refine plugin APIs
rian-be Sep 12, 2026
453067c
chore(ci): update PluginContractValidator project paths
rian-be Sep 12, 2026
eb47515
Fix: plugin validation host setup
rian-be Sep 12, 2026
c3b98b0
test(security): add plugin authentication and authorization tests
rian-be Sep 12, 2026
3958d2f
test(plugin): add infrastructure integration tests
rian-be Sep 12, 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
9 changes: 5 additions & 4 deletions .github/workflows/plugin-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ on:
tags: ['v*']
paths:
- 'src/Plugins/**'
- 'tools/PluginContractValidator/**'
- 'tools/AuthKit.PluginContractValidator/**'
- '.github/workflows/plugin-validation.yml'
pull_request:
branches: [main, development]
paths:
- 'src/Plugins/**'
- 'tools/PluginContractValidator/**'
- 'tools/AuthKit.PluginContractValidator/**'
- '.github/workflows/plugin-validation.yml'
workflow_dispatch:

env:
DOTNET_VERSION: '10.0.x'
VALIDATOR_PROJECT: 'tools/PluginContractValidator/PluginContractValidator.csproj'
VALIDATOR_PROJECT: 'tools/AuthKit.PluginContractValidator/AuthKit.PluginContractValidator.csproj'
PLUGINS_STAGING: 'out/plugins'

permissions:
Expand All @@ -42,6 +42,7 @@ jobs:
mkdir -p "${{ env.PLUGINS_STAGING }}"
for proj in $(find src/Plugins -name '*.csproj' \
-not -path '*/Abstractions/*' \
-not -path '*/Integrations/*' \
-not -path '*/obj/*' \
-not -path '*/bin/*'); do
name=$(basename "$proj" .csproj)
Expand All @@ -54,5 +55,5 @@ jobs:

- name: Run contract validation
run: |
VALIDATOR_DLL="tools/PluginContractValidator/bin/Release/net10.0/PluginContractValidator.dll"
VALIDATOR_DLL="tools/AuthKit.PluginContractValidator/bin/Release/net10.0/AuthKit.PluginContractValidator.dll"
dotnet "$VALIDATOR_DLL" "${{ github.workspace }}/${{ env.PLUGINS_STAGING }}"
4 changes: 2 additions & 2 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,5 +209,5 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Check code formatting (PluginContractValidator)
run: dotnet format tools/PluginContractValidator/PluginContractValidator.csproj --verify-no-changes --verbosity diagnostic
- name: Check code formatting (AuthKit.PluginContractValidator)
run: dotnet format tools/AuthKit.PluginContractValidator/AuthKit.PluginContractValidator.csproj --verify-no-changes --verbosity diagnostic
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ obj/
.env
/net-commander
qodana.yaml
src/Plugins/Solutions/DevTokens/manifest.json
src/Plugins/Solutions/DevTools/manifest.json
4 changes: 4 additions & 0 deletions AuthKit.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@
<Project Path="src/Plugins/Solutions/DevTokens/DevTokens.csproj" />
<Project Path="src/Plugins/Solutions/DevTools/DevTools.csproj" />
</Folder>
<Folder Name="/Plugins/Integrations/">
<Project Path="src/Plugins/Integrations/AuthKit.Plugins.Integrations.csproj" />
</Folder>
<Folder Name="/Tools/">
<Project Path="tools/AuthKit.PluginContractValidator/AuthKit.PluginContractValidator.csproj" />
</Folder>
<Project Path="src/Core/Core.csproj" />
<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>
4 changes: 4 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@
<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.AspNetCore.TestHost" 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
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ COPY ["src/Core/Core.csproj", "src/Core/"]
COPY ["src/Plugins/Abstractions/AuthKit.Plugins.Abstractions.csproj", "src/Plugins/Abstractions/"]
COPY ["src/Plugins/Solutions/DevTokens/DevTokens.csproj", "src/Plugins/Solutions/DevTokens/"]
COPY ["src/Plugins/Solutions/DevTools/DevTools.csproj", "src/Plugins/Solutions/DevTools/"]
COPY ["src/Plugins/Integrations/AuthKit.Plugins.Integrations.csproj", "src/Plugins/Integrations/"]

COPY ["tests/Host/AuthKit.Host.Tests.csproj", "tests/Host/"]
COPY ["tests/Host.IntegrationTests/AuthKit.Host.IntegrationTests.csproj", "tests/Host.IntegrationTests/"]
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 +37,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]()
48 changes: 48 additions & 0 deletions Docs/ADR/025-plugin-options-openapi-and-marten-integrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./024-plugin-lifecycle-and-hosted-services.md) | [Next]()

# [ADR-025] Keep Plugin Options, OpenAPI, And Marten Integrations Explicit

*2026-09* | Status: accepted

**Tag:** #adr_025

**Date:** 2026-09-12

**Scope:** AuthKit.Plugins.Abstractions + Host + optional plugin integrations

## Context

Plugins need strongly typed options, OpenAPI contributions, and optional Marten document configuration. These integrations have different dependency boundaries: options belong to the base contract, while Swashbuckle and Marten are infrastructure concerns.

## Decision

`IAuthKitPlugin.BindConfiguration<TOptions>` binds through the standard .NET options system from `Plugins:{Name}`. Plugins therefore receive normal `IOptions<T>`, `IOptionsSnapshot<T>`, or `IOptionsMonitor<T>` services without a custom registry.

OpenAPI and Marten integration contracts live in the optional `AuthKit.Plugins.Integrations` project:

- `IOpenApiPlugin.ConfigureOpenApi(SwaggerGenOptions)` receives the actual Swagger options owned by the Host;
- `IMartenPlugin.ConfigureMarten(StoreOptions)` receives the actual Marten options owned by the Host.

The Host invokes both hooks in stable plugin ID order. OpenAPI hooks run inside the existing `AddSwaggerGen` configuration. Marten hooks run inside the existing `AddMarten` configuration. Exceptions are allowed to fail host configuration; contributions are never silently discarded.

## Rationale

The base plugin contract stays independent of optional persistence and documentation infrastructure. Plugins that need either integration reference the optional integration project, while ordinary plugins retain the smaller abstraction dependency.

## Rejected

- Adding Swashbuckle or Marten references to the base abstractions project would force unrelated plugins to depend on optional host infrastructure.
- A custom options registry would duplicate the standard .NET options and DI mechanisms.
- Creating separate Swagger or Marten option instances would disconnect plugin contributions from the actual host configuration.

## Consequences

Plugin configuration is isolated and strongly typed. OpenAPI and Marten contributions are deterministic and share the host-owned configuration objects. Hosts without Marten do not need to reference the integration contract or invoke its hook.

## Related

- [ADR-022](./022-plugin-configuration-context-and-builder.md) - plugin configuration context
- [ADR-023](./023-plugin-application-pipeline-hooks.md) - application integration hooks
- [Issue #11](https://github.com/AuthKits/AuthKit.Server/issues/11) - options, OpenAPI, and Marten integration requirements

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./024-plugin-lifecycle-and-hosted-services.md) | [Next]()
Loading
Loading