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.
// ChioMiddleware.csproj: PackageId Backbay.Chio.Middleware, net8.0, 0.1.0
using Backbay.Chio;
builder.Services.AddChioProtection();
app.UseChioProtection();| Property | Value |
|---|---|
PackageId | Backbay.Chio.Middleware |
RootNamespace | Backbay.Chio |
TargetFramework | net8.0 |
Version | 0.1.0 |
FrameworkReference | Microsoft.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.
<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.
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.
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.
| Option | Default | Purpose |
|---|---|---|
SidecarUrl | CHIO_SIDECAR_URL env, else http://127.0.0.1:9090 | Base URL of the sidecar kernel. |
TimeoutSeconds | 5 | Sidecar HTTP timeout. |
OnSidecarError | deny | Reserved no-op. Sidecar errors always fail closed. |
IdentityExtractor | null, resolved to ChioIdentityExtractor.DefaultExtract (:80) | Custom caller extraction, typed IdentityExtractorDelegate. |
RouteResolver | null, resolved to a function returning the raw path (:81) | Maps (method, path) to a route pattern. |
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.
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.
| Member | Line | Endpoint | Returns |
|---|---|---|---|
DefaultSidecarUrl | 32 | none | http://127.0.0.1:9090 |
EvaluateAsync(request, capabilityToken?) | 56 | POST /chio/evaluate | EvaluateResponse |
VerifyReceiptAsync(receipt) | 121 | POST /chio/verify | VerifyReceiptResponse |
HealthCheckAsync() | 146 | GET /chio/health | bool |
Dispose() | 159 | none | void |
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.
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.
| Type | Line | Role |
|---|---|---|
AuthMethod | 13 | How the caller authenticated, plus the hashed credential. Factories: Anonymous, Bearer, ApiKey. |
CallerIdentity | 58 | Subject, auth method, verified flag, tenant, agent id. CreateAnonymous() builds the fallback. |
Verdict | 84 | Kernel verdict, with IsAllowed() and IsDenied(). |
VerdictType | 87 | The verdict string itself. The property is named VerdictType in C# and serializes as verdict on the wire. |
GuardEvidence | 113 | Per-guard evaluation evidence. |
HttpReceipt | 129 | Signed receipt. IsAuthorized() at line 205 encodes the mediated-authorization predicate. |
ChioHttpRequest | 216 | Request posted to /chio/evaluate. |
EvaluateResponse | 263 | Verdict, receipt, and evidence. |
VerifyReceiptResponse | 278 | Authority result from /chio/verify. Authorizes() at line 313 is the strict gate. |
ChioPassthrough | 349 | Reserved degraded-state marker with Mode, Error, and Message. Its own doc comment says the middleware always fails closed. |
ChioErrorResponse | 364 | The structured error body: error, message, and the null-omitted receipt_id and suggestion. |
ChioErrorCodes | 384 | The five error-code constants, listed under Errors. |
ChioContextKeys | 393 | One 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.
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.
Related
- 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.