Table of Contents

Step 5: Govern Endpoints

Status: Complete with AsiBackbone 7.2.1. First released in v0.1.0-alpha.5 on AsiBackbone 7.2.0; see Capability before acknowledgment for why the step now requires 7.2.1.

Outcome

The versioned GovernedOperationsController demonstrates governance on real application endpoints before their action bodies execute. It keeps a read-only description route intentionally ungoverned and places three unsafe operations behind explicit host-owned rules:

Endpoint Governance metadata Successful outcome
GET /api/v1/governed-operations None; explicitly anonymous Returns the read-only sample description.
POST /api/v1/governed-operations/administrator AdministratorActionPolicy Executes for an authenticated actor in a configured administrator role.
POST /api/v1/governed-operations/capability CapabilityProtectedActionPolicy and referenceapplication.consequential.execute capability Executes when the actor holds the declared capability.
POST /api/v1/governed-operations/consequential All three policy markers, the capability scope, and acknowledgment metadata Challenges an entitled administrator, then executes only on a same-actor, same-operation, unexpired, one-time acknowledged retry.

The POST routes keep NCAT's authenticated-by-default authorization policy and validate antiforgery tokens explicitly with [ValidateAntiForgeryToken], matching NCAT's own AccountController. Endpoint governance adds an execution boundary; it does not replace either control.

For local exploration without an external identity provider, the supplemental Development-only demo identity controls can issue a temporary cookie containing fixed, selectable walkthrough claims. The control is off by default and is not registered outside Development.

Attach endpoint metadata

Controller attributes make the protected operations and the order-independent requirements visible next to the action:

[HttpPost("consequential")]
[ValidateAntiForgeryToken]
[GovernancePolicy(typeof(AdministratorActionPolicy))]
[GovernancePolicy(typeof(CapabilityProtectedActionPolicy))]
[GovernancePolicy(typeof(ConsequentialActionPolicy))]
[RequireCapabilityGrant(ReferenceApplicationGovernancePolicySet.ConsequentialExecutionScope)]
[RequireAcknowledgment]
public ActionResult<GovernedOperationResponse> ExecuteConsequentialAction()

Routing selects that endpoint, NCAT authenticates and authorizes the request, and AsiBackbone evaluates its metadata before MVC invokes the action. A denied decision or an acknowledgment challenge therefore prevents the response text inside the action from being produced.

GET /api/v1/governed-operations is a read-only description. It is marked AllowAnonymous and has no AsiBackbone metadata, so the middleware passes it through. This is deliberate rather than an omission: reading the sample does not perform the operation being governed.

Validate capability grants

ClaimEndpointCapabilityGrantValidator is the host-owned grant boundary. AsiBackbone supplies the endpoint's declared scopes but does not infer where grants come from. The validator reads the current actor's authenticated identities and requires every declared scope to appear as an exact claim using ApplicationAuthorizationOptions.PermissionClaimType.

With the shipped NCAT configuration, authentication-provider claims such as permission, permissions, scope, or scp are normalized to application:permission. Providers should issue one claim value per capability. A scope in endpoint metadata only states what the operation requires; it never proves the actor holds that grant.

The validator preserves only a decision that is already denied. Every other decision, including one that requires acknowledgment, must pass the grant check, and a missing grant is denied with referenceapplication.capability.grant_required. AsiBackbone 7.2.1 calls the validator for every decision that is not already denied, so a missing grant always takes precedence over an acknowledgment challenge. Registration is scoped to the request:

services.AddScoped<IEndpointCapabilityGrantValidator, ClaimEndpointCapabilityGrantValidator>();

A claim proves entitlement but does not tie a grant to any particular request. The supplemental Bind capabilities to signed tokens procedure adopts AsiBackbone's second model, signed capability tokens bound to one actor, operation, and payload, and compares the two.

Map HTTP outcomes

The endpoint middleware now uses the application's Problem Details service for ordinary governance denials. The body is intentionally generic: it reports HTTP 403 without exposing policy internals, while NCAT adds its normal request and trace correlation fields.

An otherwise-allowed consequential request produces AsiBackbone's acknowledgment challenge with HTTP 428 Precondition Required. The challenge includes the handshake identifier, required acknowledgment code, and stable acknowledgment reason code. The host persists that request in AsiBackbone's acknowledgment tables with a five-minute validity window.

The actor submits the handshake identifier and required code to POST /api/v1/acknowledgments. The host delegates the actor and code checks back to IAcknowledgmentChallengeService, persists the resulting acknowledgment, and returns its identifier plus the X-ReferenceApplication-Acknowledgment retry-header name. A retry presents that identifier:

POST /api/v1/governed-operations/consequential
X-ReferenceApplication-Acknowledgment: <acknowledgment-id>

Before governance runs, the host verifies that the acknowledgment is accepted, unexpired, unused, belongs to the same actor, and names the selected endpoint's operation. The consequential decision policy then permits the already- acknowledged evaluation to continue through the ordinary capability validator. The endpoint consumes the acknowledgment immediately before executing. A second retry is denied as referenceapplication.acknowledgment.replayed.

The accepted retry's decision ledger record carries both HandshakeId and AcknowledgmentId. They are part of AsiBackbone's canonical ledger payload, so the existing record hash covers the link. The governance explorer displays both identifiers on the decision timeline.

The host-owned stable failures include referenceapplication.acknowledgment.expired, referenceapplication.acknowledgment.replayed, and referenceapplication.acknowledgment.operation_mismatch. AsiBackbone's challenge service retains its actor/code failures, including acknowledgment.challenge.actor_mismatch and acknowledgment.challenge.code_mismatch.

The effective order is:

  1. authentication and ASP.NET Core authorization;
  2. AsiBackbone policy evaluation;
  3. host-owned capability validation, for every decision that is not already denied;
  4. denial or acknowledgment challenge when required;
  5. MVC antiforgery validation when governance allows the request to continue; and
  6. MVC action execution only when every boundary permits it.

Antiforgery validation still applies to every unsafe MVC route. The tests obtain a token for the same identity that submits the POST, and separately prove that a request without one is rejected.

Capability before acknowledgment

Step 5 requires AsiBackbone 7.2.1 or later. In 7.2.0, capability validation ran only when the policy decision could proceed immediately. An acknowledgment-required decision cannot, so on the consequential endpoint the framework skipped step 3 and issued the acknowledgment challenge directly: an administrator who did not hold referenceapplication.consequential.execute received HTTP 428 instead of HTTP 403. The action body never executed, but the actor was offered an acknowledgment for an operation they were not entitled to.

AsiBackbone 7.2.1 fixed this (AsiBackbone/AsiBackbone#887). Capability validation now runs for every decision that is not already denied, so a missing grant is denied before any acknowledgment challenge. The validator needed no change for the upgrade, because it already denied a missing grant on an acknowledgment-required decision. The upgrade is tracked in AsiBackbone/ReferenceApplication#17.

Verification

GovernedOperationsEndpointTests exercises the production routes through the complete NCAT test host:

  • the anonymous read-only description passes without governance metadata;
  • an administrator is allowed and a non-administrator receives safe Problem Details;
  • an actor with the exact normalized capability claim is allowed and an actor without it is denied;
  • an administrator with the capability receives an HTTP 428 challenge, accepts it, retries, and executes;
  • the accepted retry records the handshake and acknowledgment identifiers under the decision record hash;
  • a foreign actor, wrong acknowledgment code, expired challenge, and replayed acknowledgment are denied with stable reason codes;
  • an actor with the capability but without an administrator role is denied on the consequential endpoint;
  • an administrator without the capability is denied on the consequential endpoint with safe Problem Details rather than offered an acknowledgment challenge, as described in Capability before acknowledgment;
  • an anonymous caller is rejected by ASP.NET Core authorization before governance runs; and
  • an otherwise-valid administrator request without an antiforgery token is rejected.

ClaimEndpointCapabilityGrantValidator is also tested on its own. It requires every declared scope, ignores grants on unauthenticated identities, preserves an existing denial, and denies a missing grant even when the decision already requires acknowledgment.

The complete inherited NCAT suite continues to run with the new tests:

dotnet restore ReferenceApplication.slnx --locked-mode
dotnet build ReferenceApplication.slnx --configuration Release --no-restore
dotnet test ReferenceApplication.slnx --configuration Release --no-build --no-restore

Authoritative references

Next

Step 6: Persist and verify audit evidence