Skip to content

[Task]: Authkit middleware pipeline #19

Description

@rian-be

Summary

Extend the AuthKit plugin contract with a structured middleware contract for AuthKit hosts, built on ASP.NET Core middleware primitives (HttpContext / RequestDelegate).

This sub-issue introduces the core contract types:

  • a PipelinePosition enum that declares where middleware should be inserted,
  • a PluginMiddleware registration record returned by the plugin,
  • an IsMiddlewareEnabled toggle per middleware,
  • an AuthKitMiddlewareBase abstract class for simple/convention-based implementations,
  • and an IAuthKitMiddleware interface for DI-aware (scoped) middleware.

The plugin declares what middleware it needs; the host decides how to activate it and connect it to the pipeline. Middleware activation and dependency resolution are host responsibilities.

Note: because the contract is expressed in terms of HttpContext and RequestDelegate, it is HTTP/ASP.NET Core-specific, not strictly host gnostic. A truly host agnostic abstraction would require separate adapter - C7 addresses gRPC through an interceptor adapter. C1–C5 should be read as an AuthKit host contract built on ASP.NET Core primitives.

Goal

Provide first class, strongly typed plugin APIs for contributing middleware, so that:

  • plugin middleware is ordered deterministically and predictably,
  • plugins declare middleware by type the host owns activation and resolution,
  • disabled middleware is skipped without side effects,
  • and the host pipeline remains consistent and debuggable.

No custom middleware engine should be introduced the host reuses the existing ASP.NET Core pipeline.

Background

Today plugins have no unified way to register middleware. Each plugin works around the host, leading to:

  • unpredictable ordering,
  • duplicated or conflicting registration,
  • inconsistent enable/disable semantics,
  • and middleware that cannot use scoped services.

A structured contract lets the host own ordering and diagnostics while plugins stay declarative.

Scope

C1. PipelinePosition enum

Define the insertion points available to plugins:

public enum PipelinePosition
{
    BeforeRouting,
    AfterRouting,
    BeforeAuthentication,
    AfterAuthorization,
    BeforeEndpoints,
    AfterEndpointExecution
}

C2. Multiple middleware

Let plugin return collection instead of single middleware:

IReadOnlyList<PluginMiddleware> Middlewares { get; }

where PluginMiddleware declares the middleware type and its position.

C3. IsMiddlewareEnabled

Each PluginMiddleware has an IsMiddlewareEnabled flag. Disabled entries are skipped by the host.

C4. AuthKitMiddlewareBase

A convenience/convention-based abstract class simplifying implementation:

public abstract class AuthKitMiddlewareBase
{
    public abstract Task InvokeAsync(HttpContext context, RequestDelegate next);
}

C5. IAuthKitMiddleware (DI-aware)

A DI aware interface for middleware that needs scoped services:

public interface IAuthKitMiddleware
{
    Task InvokeAsync(HttpContext context, RequestDelegate next);
}

The host resolves an implementation from the request service provider, so constructor-injected scoped services participate in the same request scope.


C1. PipelinePosition

Proposed Contract

public enum PipelinePosition
{
    BeforeRouting,
    AfterRouting,
    BeforeAuthentication,
    AfterAuthorization,
    BeforeEndpoints,
    AfterEndpointExecution
}

Host Pipeline Mapping

Each value maps to well defined point in the standard ASP.NET Core pipeline:

BeforeRouting -> before UseRouting
AfterRouting -> after UseRouting, before authentication
BeforeAuthentication -> before UseAuthentication
AfterAuthorization -> after UseAuthorization (ie. after authentication AND authorization)
BeforeEndpoints -> before UseEndpoints
AfterEndpointExecution -> post-endpoint execution (response post-processing)

AfterEndpointExecution explicit semantics

AfterEndpointExecution represents post endpoint execution: the middleware runs on the return/response path after the endpoint has executed. It must not be interpreted merely as "registered after UseEndpoints()". The host must treat it as response post processing, not as a second independent pipeline tail.

AfterAuthorization explicit semantics

ASP.NET Core has two distinct steps, UseAuthentication() and UseAuthorization(). AfterAuthorization means after both after authentication and authorization. There is intentionally no separate AfterAuthentication position plugin requiring "after authentication but before authorization" cannot be expressed and must be documented as unsupported.

Deterministic Ordering

Within single PipelinePosition, the host orders middleware by:

Order
  → PluginId (stable plugin identifier)
    → DeclarationIndex

PluginId must be stable identifier defined by the plugin contract and must not depend on assembly load order or runtime object identity. Ordering must not depend on filesystem ordering, dictionary enumeration, or undefined plugin discovery order.


C2. PluginMiddleware Model

Proposed Contract

public sealed record PluginMiddleware(
    Type MiddlewareType,
    PipelinePosition Position,
    int Order = 0,
    bool IsMiddlewareEnabled = true,
    string? Name = null);

The plugin declares the middleware type and its position. There is no Factory and no PluginMiddlewareInstance in the contract: middleware activation and dependency resolution are host responsibilities.

Plugin declares WHAT, host decides HOW

PLUGIN
  declares: MiddlewareType + Position + Order + Enabled + Name

HOST
  validates
  filters disabled
  deterministic sort (Order, PluginId, DeclarationIndex)
  resolves from request IServiceProvider
  adapts to ASP.NET Core pipeline

The plugin must not control the host pipeline construction mechanism. It only supplies the type; the host decides how to construct and connect it (eg. UseMiddleware<T>() or resolving IAuthKitMiddleware from the request services).

Host Aggregation

The host collects PluginMiddleware from every plugin and builds one ordered pipeline. A middleware that throws at construction or registration time must surface an explicit error identifying the owning plugin.


C3. IsMiddlewareEnabled

Behavior

When IsMiddlewareEnabled is false, the host must not insert the middleware and must not allocate its instance. This is distinct from a middleware that is present but no op.

Disabled middleware must not affect the ordering of other middleware.


C4. AuthKitMiddlewareBase

Design

public abstract class AuthKitMiddlewareBase
{
    public abstract Task InvokeAsync(HttpContext context, RequestDelegate next);
}

This is the convention-based / convenience option: plugins implement InvokeAsync directly without needing DI. It does not imply "no DI" the host may still make scoped services available through its standard activation mechanism; the base class simply removes the need for the plugin to implement an interface.


C5. IAuthKitMiddleware (DI aware)

Proposed Contract

public interface IAuthKitMiddleware
{
    Task InvokeAsync(HttpContext context, RequestDelegate next);
}

Host Resolution single request scope

The host resolves IAuthKitMiddleware from the request service provider (the IServiceProvider already flowing through the request), so the middleware's scoped dependencies (DbContext, repositories, etc.) participate in the same request scope as the rest of the request:

HTTP Request
      │
      ▼
Request IServiceProvider
      ├── AuthKit middleware
      ├── Plugin services
      ├── DbContext
      └── repositories

The host must not create separate "plugin scope" per middleware. Doing so would place middleware's DbContext in a different scope than the rest of the request, causing lifetime/leak issues. Middleware is activated within the existing request scope.

Signature Note

The delegate parameter must be named next (the downstream RequestDelegate) to match ASP.NET Core conventions and the contract validator (C6).


Host Pipeline Integration

C1–C5 define the contract. The host is responsible for:

  • reading Middlewares from each plugin,
  • validating each entry (see C6),
  • filtering disabled entries,
  • ordering deterministically (Order → stable PluginId → DeclarationIndex),
  • inserting at the correct PipelinePosition,
  • resolving IAuthKitMiddleware from the request service provider (single scope),
  • and skipping disabled entries.

Plugin middleware must compose with host middleware and with other plugins' middleware without overriding unrelated pipeline configuration.

Key Principle

PluginMiddleware declares the middleware type. Middleware activation and dependency resolution are host responsibilities. Plugins must not control the host pipeline construction mechanism.

Backward Compatibility

This sub issue is additive.

  • Existing plugins without Middlewares continue to compile and load.
  • IAuthKitPlugin keeps its existing members; Middlewares is new optional member (default empty).
  • Default interface implementations or host fallbacks may be used so existing plugins need no changes.
  • DevTokens must continue to compile and load successfully.

Validation

C1. PipelinePosition

  • PipelinePosition enum exists with the six values.
  • Each value maps to a documented pipeline point.
  • AfterEndpointExecution is explicitly post-endpoint (response) execution, not merely registration after UseEndpoints().
  • AfterAuthorization is explicitly after authentication and authorization.
  • Host inserts middleware at the declared position.

C2. Multiple middleware

  • IReadOnlyList<PluginMiddleware> Middlewares exists.
  • PluginMiddleware carries MiddlewareType, Position, Order, IsEnabled, Name.
  • No Factory / PluginMiddlewareInstance exists in the contract.
  • Host aggregates and orders middleware deterministically.
  • All declared middleware are inserted in correct order.

C3. IsMiddlewareEnabled

  • Disabled middleware is not inserted.
  • Disabled middleware does not affect ordering.
  • Re enabling does not require code changes elsewhere.

C4. AuthKitMiddlewareBase

  • Abstract base class with InvokeAsync(HttpContext, RequestDelegate) exists.
  • It is documented as the convention-based/convenience option (not "no DI").
  • Plugins can implement it without DI.

C5. IAuthKitMiddleware

  • DI-aware interface with InvokeAsync(HttpContext, RequestDelegate) exists.
  • Host resolves it from the request service provider.
  • Scoped services share the request scope (no separate plugin scope per middleware).
  • The plugin cannot control the construction mechanism.

Integration

  • Plugin middleware works alongside host middleware.
  • Ordering is deterministic across plugins (Order -> PluginId -> DeclarationIndex).
  • DevTokens continues to load.
  • PluginContractValidator passes.

Acceptance Criteria

  • Plugins can declare middleware via PipelinePosition and PluginMiddleware (type based, no Factory).
  • PipelinePosition unambiguously defines insertion points, including explicit AfterEndpointExecution and AfterAuthorization semantics.
  • Multiple middleware per plugin are supported and ordered deterministically (Order -> stable PluginId -> DeclarationIndex).
  • Disabled middleware is skipped without side effects.
  • Plugins implement middleware via AuthKitMiddlewareBase (convention) or IAuthKitMiddleware (DI aware).
  • DI aware middleware uses scoped services from the request service provider (single scope).
  • The plugin declares what the host owns activation and connection.
  • Host insertion follows the declared positions.
  • Existing plugins remain backward compatible.
  • PluginContractValidator passes.
  • DevTokens continues to compile and load successfully.

Non Goals

This sub issue does not include:

  • a Factory or PluginMiddlewareInstance mechanism (the host owns construction),
  • a separate "plugin scope" per middleware,
  • the contract validator implementation (see C6),
  • gRPC pipeline mapping (see C7),
  • endpoint mapping or routing policy,
  • authentication/authorization hooks,
  • or custom middleware engine.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1Core operationadditiveAdditive, non-breaking changearea/abstractionsAuthKit.Plugins.Abstractions contractcontractChanges the plugin contractsub-taskChild task of an epic

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions