Skip to content

feat(plugin): add authentication, options, and infrastructure integration hooks - #36

Merged
sean6224 merged 22 commits into
developmentfrom
Host-Hook
Sep 13, 2026
Merged

sean6224 merged 22 commits into
developmentfrom
Host-Hook

Conversation

@rian-be

@rian-be rian-be commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Continuation of the plugin host integration after the Host Lifecycle work: adds plugin contracts for contributing to host-owned configuration strongly typed options binding, authentication and authorization hooks plus optional Marten and OpenAPI integration interfaces. The host now consults plugins when building those infrastructures, with dedicated contract and security tests and two ADRs.

Stacked on #35 (Host-Lifecycle). This branch builds on the lifecycle/configuration hooks work merged via #35, so the diff against development shows those commits too until #35 lands. After that, only the additions below remain.

Plugin Integration Contracts

  • adds IAuthKitPlugin.ConfigureAuthentication(AuthenticationBuilder) and IAuthKitPlugin.ConfigureAuthorization(AuthorizationOptions) optional hooks with default noop implementations
  • adds IAuthKitPlugin.BindConfiguration<TOptions> and PluginExtensions.BindConfiguration<TOptions> binding options from the standard Plugins:{Name} section
  • adds the AuthKit.Plugins.Integrations project with IMartenPlugin and IOpenApiPlugin for contributing to the host Marten store and Swagger generator options

Host Configuration Integration

  • invokes IMartenPlugin configuration while the host builds its Marten store
  • invokes IOpenApiPlugin configuration while building the Swagger generator
  • invokes plugin authentication and authorization hooks from the Keycloak configuration path
  • wires the plugin hooks into Program and references the integrations project from the Host

Validation

  • 101/101 tests pass (24 Abstractions, 68 Host, 9 Integration)
  • dotnet build completes with zero errors across the solution
  • git diff --check passes

Documentation

  • adds ADR-025 for plugin options, OpenAPI, and Marten integrations
  • adds ADR-026 for plugin authentication and authorization hooks

Result

Plugins can now bind their options directly and contribute to host-owned authentication, authorization, Marten, and OpenAPI configuration without taking ownership of those infrastructures.

Closes #11
Closes #12
Closes #13

@rian-be rian-be added enhancement New feature or request contract Changes the plugin contract additive Additive, non-breaking change area/host Host-side runtime (DI, OpenAPI, health exec) area/abstractions AuthKit.Plugins.Abstractions contract labels Sep 12, 2026
Comment thread src/Host/Plugins/PluginApplicationConfiguration.cs Fixed
Comment thread src/Host/Plugins/PluginApplicationConfiguration.cs Fixed
Comment thread src/Host/Plugins/PluginLifecycleHostedService.cs Fixed
Comment thread src/Host/Plugins/PluginLifecycleHostedService.cs Fixed
Comment thread src/Host/Plugins/PluginLifecycleHostedService.cs Fixed
Comment thread src/Host/Plugins/PluginLifecycleHostedService.cs Fixed
The default interface member cached the capability set in a static field
shared by every plugin implementation, so the first plugin's capabilities
leaked to all others. Compute the set per instance instead and add a
regression test covering two plugins with disjoint capabilities.
Comment on lines +13 to +89
extension(IAuthKitPlugin plugin)
{
/// <summary>
/// Binds strongly typed options from the plugin's configuration section.
/// </summary>
/// <typeparam name="TOptions">The plugin options type.</typeparam>
/// <param name="services">The host service collection.</param>
/// <param name="configuration">The application configuration.</param>
/// <remarks>
/// <para>
/// The section is resolved through <see cref="GetPluginConfiguration"/>: the
/// <c>Plugins:{Id}</c> section wins when it exists, otherwise the
/// <c>Plugins:{Name}</c> section is used. This matches the scoping used by
/// <see cref="AuthKitPluginContext"/>.
/// </para>
/// </remarks>
public void BindConfiguration<TOptions>(IServiceCollection services,
IConfiguration configuration)
where TOptions : class
{
ArgumentNullException.ThrowIfNull(plugin);
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);

services.Configure<TOptions>(plugin.GetPluginConfiguration(configuration));
}

/// <summary>
/// Resolves the configuration section scoped to a plugin.
/// </summary>
/// <param name="configuration">The application configuration.</param>
/// <returns>
/// The <c>Plugins:{Id}</c> section when it has children; otherwise, the
/// <c>Plugins:{Name}</c> section.
/// </returns>
/// <remarks>
/// <para>
/// Plugin sections live under the Plugins root. The stable plugin ID is
/// checked first an empty or missing ID section falls back to the plugin name.
/// This is the single resolution rule used by the host
/// (<see cref="AuthKitPluginContext"/>) and by options binding.
/// </para>
/// </remarks>
public IConfiguration GetPluginConfiguration(IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(plugin);
ArgumentNullException.ThrowIfNull(configuration);

var plugins = configuration.GetSection("Plugins");
var byId = plugins.GetSection(plugin.Id);

return byId.GetChildren().Any()
? byId
: plugins.GetSection(plugin.Name);
}

/// <summary>
/// Checks if the plugin supports the specified capability.
/// </summary>
/// <param name="capability">The ability to check.</param>
/// <returns>true if the plugin supports the capability; otherwise, <c>false</c>.</returns>
/// <remarks>The comparison is case-insensitive.</remarks>
public bool Supports(string capability) =>
plugin == null
? throw new ArgumentNullException(nameof(plugin))
: plugin.Capabilities.Contains(capability, StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Checks if the plugin has the specified dependency.
/// </summary>
/// <param name="dependencyId">The dependency ID to check.</param>
/// <returns><c>true</c> if the plugin depends on the specified dependency; otherwise, <c>false</c>.</returns>
public bool HasDependency(string dependencyId) =>
plugin == null
? throw new ArgumentNullException(nameof(plugin))
: plugin.DependsOn.Contains(dependencyId, StringComparer.OrdinalIgnoreCase);
}
Comment on lines +92 to +116
extension(PluginManifest manifest)
{
/// <summary>
/// Checks if the plugin manifest supports the specified capability.
/// </summary>
/// <param name="capability">The ability to check.</param>
/// <returns>true if the manifest supports the capability; otherwise, <c>false</c>.</returns>
/// <remarks>
/// The comparison is case-insensitive.
/// </remarks>
public bool Supports(string capability) =>
manifest == null
? throw new ArgumentNullException(nameof(manifest))
: manifest.Capabilities.Contains(capability, StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Checks if the plugin manifest has the specified dependency.
/// </summary>
/// <param name="dependencyId">The dependency ID to check.</param>
/// <returns><c>true</c> if the manifest declares the specified dependency; otherwise, <c>false</c>.</returns>
public bool HasDependency(string dependencyId) =>
manifest == null
? throw new ArgumentNullException(nameof(manifest))
: manifest.DependsOn.Contains(dependencyId, StringComparer.OrdinalIgnoreCase);
}
@rian-be
rian-be requested a review from RX-J September 12, 2026 22:04
@sean6224
sean6224 merged commit a89c8bc into development Sep 13, 2026
9 checks passed
@sean6224
sean6224 deleted the Host-Hook branch September 13, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

additive Additive, non-breaking change area/abstractions AuthKit.Plugins.Abstractions contract area/host Host-side runtime (DI, OpenAPI, health exec) contract Changes the plugin contract enhancement New feature or request

Projects

None yet

4 participants