Chio/Docs
LOGIN · JOIN

ReferenceSDKs

.NET SDK

Backbay.Chio.Middleware: fail-closed ASP.NET Core middleware that evaluates each request against the Chio sidecar and verifies the signed receipt.

Source

This page reflects the project at sdks/dotnet/ChioMiddleware/src in the chio repository: ChioMiddleware.csproj, ChioMiddlewareExtensions.cs, ChioSidecarClient.cs, ChioTypes.cs, and ChioIdentityExtractor.cs. The package id, root namespace, target framework, version, and framework reference render from the sdks dataset at the pin. The worked application is examples/hello-dotnet. The project carries no specification status line and no RFC 2119 keywords; the C# source is the truth for every name below.


Synopsis

Register the services, then insert the middleware. Both extension methods live in the Backbay.Chio namespace.

csharp
// ChioMiddleware.csproj: PackageId Backbay.Chio.Middleware, net8.0, 0.1.0
using Backbay.Chio;

builder.Services.AddChioProtection();
app.UseChioProtection();
PropertyValue
PackageIdBackbay.Chio.Middleware
RootNamespaceBackbay.Chio
TargetFrameworknet8.0
Version0.1.0
FrameworkReferenceMicrosoft.AspNetCore.App

The package id and the namespace differ: types are imported from Backbay.Chio, the RootNamespace the csproj sets. The csproj declares that one framework reference and no PackageReference, so the middleware pulls in no third-party runtime dependency.


Installation

Reference the middleware project from the web project. examples/hello-dotnet does this, and it is the whole of that example's dependency graph.

examples/hello-dotnet/HelloChio.csproj:9-11xml
  <ItemGroup>
    <ProjectReference Include="../../sdks/dotnet/ChioMiddleware/src/ChioMiddleware.csproj" />
  </ItemGroup>

The middleware needs a running Chio sidecar. ChioSidecarClient.DefaultSidecarUrl is http://127.0.0.1:9090 (ChioSidecarClient.cs:32), and the constructor prefers the CHIO_SIDECAR_URL environment variable over it when no base URL is passed (ChioSidecarClient.cs:40).

Quickstart

AddChioProtection (ChioMiddlewareExtensions.cs:269) registers the services and UseChioProtection (:287) inserts the middleware.

sdks/dotnet/ChioMiddleware/README.md:29-39csharp
using Backbay.Chio;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddChioProtection();

var app = builder.Build();
app.UseChioProtection();

app.MapGet("/pets", () => new { pets = Array.Empty<object>() });

app.Run();

The middleware fails closed. A sidecar it cannot reach produces a 502 and a structured JSON body (ChioMiddlewareExtensions.cs:169, :176, :186). A denied evaluation returns the verdict's HttpStatus when it is positive and 403 otherwise (:198). An allowed request reaches the next component only after the embedded receipt passes the authorization check and the signature check, and the middleware then sets X-Chio-Receipt-Id to the receipt id (:210).

The middleware evaluates seven verbs: GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS (ChioMiddlewareExtensions.cs:69). Any other method short-circuits with a 405 and the error code chio_evaluation_failed before the sidecar is contacted (:97).

Excluding a route

UseChioProtection governs everything downstream of where it sits. To leave a health endpoint out, map it before the middleware and branch with UseWhen. The worked example does this.

examples/hello-dotnet/HelloApp.cs:7-22csharp
    internal static WebApplication Create(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.Services.AddChioProtection();

        var app = builder.Build();
        app.MapGet("/healthz", HelloEndpoints.Health);
        app.UseWhen(
            context => RequiresChioProtection(context.Request.Path),
            branch => branch.UseChioProtection());
        app.MapHelloEndpoints();
        return app;
    }

    internal static bool RequiresChioProtection(PathString path) =>
        !path.Equals("/healthz", StringComparison.OrdinalIgnoreCase);

Run it with examples/run-hello-smokes.sh hello-dotnet, which starts a trust control plane, the application, and a sidecar, then walks the three routes. GET /hello returns 200 with the receipt id on X-Chio-Receipt-Id; POST /echo without a capability returns 403 and a ChioErrorResponse body carrying chio_access_denied, the deny reason, the receipt id, and the fixed suggestion provide a valid capability token in the X-Chio-Capability header or chio_capability query parameter (ChioMiddlewareExtensions.cs:204).

Configuration

AddChioProtection accepts an Action<ChioMiddlewareOptions> configure callback. The options type is at ChioMiddlewareExtensions.cs:22-52.

OptionDefaultPurpose
SidecarUrlCHIO_SIDECAR_URL env, else http://127.0.0.1:9090Base URL of the sidecar kernel.
TimeoutSeconds5Sidecar HTTP timeout.
OnSidecarErrordenyReserved no-op. Sidecar errors always fail closed.
IdentityExtractornull, resolved to ChioIdentityExtractor.DefaultExtract (:80)Custom caller extraction, typed IdentityExtractorDelegate.
RouteResolvernull, resolved to a function returning the raw path (:81)Maps (method, path) to a route pattern.
csharp
builder.Services.AddChioProtection(opts =>
{
    opts.SidecarUrl = "http://127.0.0.1:9090";
    opts.TimeoutSeconds = 5;
    opts.RouteResolver = (method, path) =>
        path.StartsWith("/pets/") ? "/pets/{petId}" : path;
});

The middleware reads a capability token from the X-Chio-Capability header (ChioMiddlewareExtensions.cs:246) or the chio_capability query parameter (:255) and forwards it to the sidecar.

Identity extraction

ChioIdentityExtractor.DefaultExtract (ChioIdentityExtractor.cs:46) reads, in order, an Authorization: Bearer token, an X-API-Key header in three casings, the first cookie, then falls back to CallerIdentity.CreateAnonymous(). Each branch hashes the credential with Sha256Hex (:25 and :33) and records the method on AuthMethod, so the raw secret never reaches the sidecar. Override it with an IdentityExtractorDelegate.

csharp
builder.Services.AddChioProtection(opts =>
{
    opts.IdentityExtractor = request =>
    {
        var tenant = request.Headers["X-Tenant"].FirstOrDefault();
        return new CallerIdentity
        {
            Subject = request.Headers["X-User"].FirstOrDefault() ?? "anonymous",
            AuthMethod = AuthMethod.Bearer("..."),
            Tenant = tenant,
        };
    };
});

ChioSidecarClient

The middleware wraps ChioSidecarClient (ChioSidecarClient.cs:30), which also runs standalone. It serializes with JsonNamingPolicy.SnakeCaseLower and drops null fields to match the sidecar wire contract (:46-50). It implements IDisposable; Dispose (:159) disposes the inner HttpClient, so construct it with using.

MemberLineEndpointReturns
DefaultSidecarUrl32nonehttp://127.0.0.1:9090
EvaluateAsync(request, capabilityToken?)56POST /chio/evaluateEvaluateResponse
VerifyReceiptAsync(receipt)121POST /chio/verifyVerifyReceiptResponse
HealthCheckAsync()146GET /chio/healthbool
Dispose()159nonevoid

The optional second argument to EvaluateAsync is the capability token. When it is neither null nor whitespace the client adds an X-Chio-Capability header to the evaluation request (ChioSidecarClient.cs:65-68).

EvaluateAsync does not trust an allow-shaped response on its face. When the verdict or the embedded receipt looks like an allow, it re-verifies through /chio/verify and requires VerifyReceiptResponse.Authorizes to hold before returning; otherwise it throws ChioSidecarException with code chio_invalid_receipt (ChioSidecarClient.cs:97-114). VerifyReceiptAsync swallows a non-success status and a transport failure alike and returns a default VerifyReceiptResponse, whose booleans are all false, so a failed verification reads as a refusal.

csharp
using Backbay.Chio;

using var client = new ChioSidecarClient("http://127.0.0.1:9090");

var request = new ChioHttpRequest
{
    RequestId = Guid.NewGuid().ToString(),
    Method = "GET",
    RoutePattern = "/pets",
    Path = "/pets",
    Caller = CallerIdentity.CreateAnonymous(),
    Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
};

var result = await client.EvaluateAsync(request);
if (result.Verdict.IsAllowed() && result.Receipt.IsAuthorized())
{
    Console.WriteLine(result.Receipt.Id);
}

Types

The type graph lives in ChioTypes.cs under the Backbay.Chio namespace. Each type carries JsonPropertyName bindings to the snake_case wire names.

TypeLineRole
AuthMethod13How the caller authenticated, plus the hashed credential. Factories: Anonymous, Bearer, ApiKey.
CallerIdentity58Subject, auth method, verified flag, tenant, agent id. CreateAnonymous() builds the fallback.
Verdict84Kernel verdict, with IsAllowed() and IsDenied().
VerdictType87The verdict string itself. The property is named VerdictType in C# and serializes as verdict on the wire.
GuardEvidence113Per-guard evaluation evidence.
HttpReceipt129Signed receipt. IsAuthorized() at line 205 encodes the mediated-authorization predicate.
ChioHttpRequest216Request posted to /chio/evaluate.
EvaluateResponse263Verdict, receipt, and evidence.
VerifyReceiptResponse278Authority result from /chio/verify. Authorizes() at line 313 is the strict gate.
ChioPassthrough349Reserved degraded-state marker with Mode, Error, and Message. Its own doc comment says the middleware always fails closed.
ChioErrorResponse364The structured error body: error, message, and the null-omitted receipt_id and suggestion.
ChioErrorCodes384The five error-code constants, listed under Errors.
ChioContextKeys393One constant, Passthrough, whose value is the string ChioPassthrough. It names an HttpContext.Items key.

Nothing under sdks/dotnet/ChioMiddleware/src writes ChioContextKeys.Passthrough or constructs a ChioPassthrough: the two are declared and unused, which is what the marker's doc comment means by reserved. A consumer reading HttpContext.Items for that key finds nothing, because the middleware denies instead of passing through.

The authorization predicates

HttpReceipt.IsAuthorized() (ChioTypes.cs:205-210) requires ReceiptKind mediated_decision, BoundaryClass prevent, a null ObservationOutcome, TrustLevel mediated, and an allow verdict. VerifyReceiptResponse.Authorizes() (:313-327) layers on top: the verify response must carry Ok, Authorized, SignatureValid, SignerTrusted, ReceiptIdValid, and ParameterHashValid, the same three classification strings, a Result of allow, the receipt's own predicate, a lower-hex 64-character id and content hash, and a non-empty signature. The Receipt format page carries the full field set.

Errors

ChioSidecarException carries a Code and an optional StatusCode. The middleware maps it to the response: an invalid receipt becomes a 502 with chio_invalid_receipt, any other sidecar failure a 502 with chio_sidecar_unreachable, and a denied verdict the verdict status with chio_access_denied.

csharp
try
{
    var result = await client.EvaluateAsync(request);
}
catch (ChioSidecarException ex)
{
    Console.WriteLine($"{ex.Code}: {ex.Message}");
}

ChioErrorCodes (ChioTypes.cs:384-391) holds the codes: chio_access_denied, chio_sidecar_unreachable, chio_evaluation_failed, chio_invalid_receipt, and chio_timeout. Each is the string the sidecar returns, and each matches the Schemas and errors taxonomy.

  • SDKs: the other language bindings and their package names.
  • JVM SDK: the same middleware shape for a servlet container.
  • HTTP substrate: the sidecar endpoints this client posts to.
  • Receipt format: the fields the receipt types bind to.
  • Bindings API: the cross-language invariants every binding conforms against.