Skip to content

[Refactor] Make authenticated service identity contract body-independent across invoke/binding endpoints #362

@louis4li

Description

@louis4li

✅ Status: largely resolved (2026-04-23, PR #311)

This issue was filed at 2026-04-24 02:47 UTC; PR #311 merged 2026-04-24 17:18 UTC. The PR's commit 2ba2b2ae ("Harden service invoke and binding identity handling", 2026-04-23) had already landed the two main residues described below. After-the-fact audit confirms:

A. Invoke caller identity — FIXED

src/platform/Aevatar.GAgentService.Hosting/Endpoints/ServiceEndpoints.cs:452-475 — new ResolveInvocationCaller. When authenticated, body's callerServiceKey / callerTenantId / callerAppId are ignored and claims are used (ServiceKey set to empty string until a verifiable caller-service contract exists). Unauthenticated path still falls back to body.

  • Test: ServiceEndpointsTests.cs:1083 InvokeAsync_WhenAuthenticatedIdentityConflictsWithBody_ShouldIgnoreSpoofedCallerIdentity

B. Bound-service identity mismatch — FIXED (loud, not silent)

src/platform/Aevatar.GAgentService.Governance.Hosting/Endpoints/ServiceBindingEndpoints.cs:193-221 — new TryValidateBoundServiceIdentity. With an authenticated context, request.Service.{TenantId,AppId,Namespace} mismatching claims now returns 400 BOUND_SERVICE_IDENTITY_CONFLICT, not silent rewrite.

  • Test: GovernanceEndpointsTests.cs:104 BindingEndpoints_WhenAuthenticatedBoundServiceIdentityConflictsWithClaims_ShouldReturnBadRequest

C. Remaining residue — partially open

Two items from the original Acceptance Criteria are not yet addressed:

  1. Binding owner identity is still claim-silent-overrides-body, asymmetric with bound-service identity which is now loud. ServiceBindingEndpoints.cs resolves owner via ServiceIdentityEndpointAccess.TryResolveContext → claims win silently when present.
  2. DTO field exposure: InvokeServiceHttpRequest, BoundServiceHttpRequest, and friends still expose TenantId / AppId / Namespace as caller-controlled fields. The C# layer no longer trusts them, but the contract advertises them — half-contract per the original acceptance criterion "DTOs should stop advertising cross-tenant identity fields".

Recommendation

Close this issue and (if pursued) split the residue into two narrower issues:

  • "Make binding owner identity mismatch loud (parallel to bound-service)"
  • "Prune body-controlled identity fields from /api/services and governance DTOs"

Both are smaller-scope and easier to land than this combined issue.


Original issue below for history.

Background

PR #311 hardens the target service identity for authenticated /api/services/** and governance endpoints by making tenant_id / app_id / namespace claims authoritative.

That closes the target-side spoofing gap, but the latest review still shows two contract-level inconsistencies on the same surface:

  1. HandleInvokeAsync still accepts caller identity from the request body (callerServiceKey / callerTenantId / callerAppId) even when the request is authenticated.
  2. Service binding endpoints silently rewrite request.Service.{TenantId,AppId,Namespace} to the authenticated caller identity while still keeping the body serviceId, which leaves a confusing half-contract.

Both issues point at the same structural problem:

authenticated service identity is not yet modeled as one explicit, body-independent contract across invoke + binding paths.

Problem

1. Caller identity on invoke is still body-controlled

src/platform/Aevatar.GAgentService.Hosting/Endpoints/ServiceEndpoints.cs

The invoke path still builds ServiceInvocationCaller from body fields:

  • CallerServiceKey
  • CallerTenantId
  • CallerAppId

That value flows into DefaultInvokeAdmissionEvaluator, so an authenticated caller can still spoof an allowed caller key through the body.

This is the same spoofing class PR #311 is trying to eliminate on the target side.

2. Bound-service identity mismatch is silently rewritten

src/platform/Aevatar.GAgentService.Governance.Hosting/Endpoints/ServiceBindingEndpoints.cs

When authenticated, the endpoint currently lets claims win over body TenantId/AppId/Namespace, but keeps the requested serviceId.

So a client can believe it is binding to another tenant's service while the server silently rewrites ownership context and creates an in-tenant reference to the same serviceId string.

That behavior is hard to reason about:

  • if cross-tenant binding is illegal, explicit mismatch should fail loudly
  • if those fields are not allowed from clients, they should not stay in the DTO as dead or misleading inputs

3. Silent override hides client bugs

Today several identity helpers let authenticated claims silently override conflicting body/query values.

That is secure enough in the narrow sense, but it creates a poor contract:

  • misconfigured clients still get 202 / success responses
  • writes land under the authenticated identity instead of the body identity the caller thought they used
  • debugging becomes harder because the API accepts requests whose identity payload is semantically wrong

Goal

Make authenticated service identity a single explicit contract across invoke and binding-related endpoints:

  • authenticated caller identity must come from authenticated context, not body fields
  • explicit body/query identity mismatches should be rejected instead of silently rewritten
  • DTOs should stop advertising cross-tenant identity fields if those fields are not actually caller-controlled

Proposed scope

A. Invoke caller identity

For authenticated invoke requests:

  • derive caller identity from authenticated service claims (or an equivalent authenticated caller-service contract)
  • stop trusting callerServiceKey / callerTenantId / callerAppId from the request body as authoritative inputs
  • add integration coverage proving authenticated caller spoofing is rejected or ignored safely

B. Bound service identity semantics

For authenticated service binding requests:

  • decide one contract and make it explicit:
    • either reject explicit request.Service.{TenantId,AppId,Namespace} mismatches with 400, or
    • remove those fields from the caller-facing DTO / request model if cross-tenant binding is not supported
  • do not keep the current silent rewrite behavior as the long-term API contract

C. Claim/body mismatch policy

Across the affected service/governance endpoints:

  • authenticated identity mismatches should be loud and diagnosable
  • avoid claim wins silently when the body/query explicitly says something else
  • return a clear client error for identity conflicts where appropriate

Acceptance criteria

  • authenticated invoke no longer authorizes from body-controlled caller identity
  • integration tests cover authenticated caller spoofing attempts on invoke
  • binding endpoints no longer silently rewrite explicit bound-service tenant/app/namespace mismatches as the steady-state contract
  • the API contract clearly states whether cross-tenant binding is unsupported or supported through an explicit mechanism
  • authenticated identity mismatch behavior is consistent and diagnosable across the affected endpoints

Out of scope

Related

  • PR Fix service identity spoofing for /api/services endpoints #311
  • src/platform/Aevatar.GAgentService.Hosting/Endpoints/ServiceEndpoints.cs
  • src/platform/Aevatar.GAgentService.Governance.Hosting/Endpoints/ServiceBindingEndpoints.cs
  • src/platform/Aevatar.GAgentService.Governance.Infrastructure/Admission/DefaultInvokeAdmissionEvaluator.cs

One-line outcome

After this issue, authenticated service identity should be one explicit, body-independent contract across invoke and binding flows, instead of a mix of claim precedence plus body fallback/silent rewrite.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions